Packages
phoenix_kit
2.52.2
2.64.0
2.63.0
2.62.2
2.62.1
2.62.0
2.61.0
2.60.3
2.60.2
2.60.1
2.60.0
2.59.0
2.58.0
2.57.1
2.57.0
2.56.1
2.56.0
2.55.1
2.55.0
2.54.2
2.54.1
2.54.0
2.53.0
2.52.2
2.52.1
2.52.0
2.51.0
2.50.0
2.49.1
2.49.0
2.48.0
2.47.0
2.46.0
2.45.0
2.44.0
2.43.1
2.43.0
2.42.1
2.42.0
2.41.6
2.41.4
2.41.3
2.41.2
2.41.1
2.41.0
2.40.1
2.40.0
2.39.0
2.38.1
2.38.0
2.37.5
2.37.4
2.37.3
2.37.2
2.37.1
2.37.0
2.36.1
2.36.0
2.35.0
2.34.0
2.33.0
2.32.1
2.32.0
2.31.1
2.31.0
2.30.0
2.29.1
2.29.0
2.28.2
2.28.1
2.28.0
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.0
2.23.3
2.23.2
2.23.1
2.23.0
2.22.24
2.22.23
2.22.22
2.22.21
2.22.20
2.22.19
2.22.18
2.22.17
2.22.16
2.22.15
2.22.14
2.22.13
2.22.12
2.22.11
2.22.10
2.22.9
2.22.8
2.22.7
2.22.6
2.22.5
2.22.4
2.22.3
2.22.2
2.22.1
2.22.0
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.0
2.19.0
2.18.1
2.18.0
2.17.0
2.16.0
2.15.1
2.15.0
2.14.2
2.14.1
2.14.0
2.13.19
2.13.18
2.13.17
2.13.16
2.13.15
2.13.13
2.13.12
2.13.11
2.13.10
2.13.9
2.13.8
2.13.7
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.1
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.7.236
1.7.235
1.7.234
1.7.233
1.7.232
1.7.231
1.7.230
1.7.229
1.7.228
1.7.227
1.7.226
1.7.225
1.7.224
1.7.223
1.7.222
1.7.221
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
1.7.208
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/schemas/dimension.ex
defmodule PhoenixKit.Modules.Storage.Dimension do
@moduledoc """
Schema for dimension presets for automatic file variant generation.
Dimensions define target sizes and quality settings for automatically
generating different versions of uploaded images and videos.
## Fields
- `name` - Unique name for this dimension (e.g., "thumbnail", "medium")
- `width` - Target width in pixels (nullable = maintain aspect ratio)
- `height` - Target height in pixels (nullable = maintain aspect ratio)
- `quality` - Quality setting (1-100 for images, CRF 0-51 for videos)
- `format` - Output format override (nullable = preserve original)
- `applies_to` - What this dimension applies to: "image", "video", or "both"
- `enabled` - Whether this dimension is active
- `order` - Display order in admin interface
## Quality Settings
### Images (JPEG, WebP, PNG)
- Range: 1-100
- 85 = Good quality (default)
- 95 = High quality
- 70 = Medium quality
### Videos (MP4, WebM)
- Range: 0-51 (CRF - Constant Rate Factor)
- Lower = Higher quality
- 23 = Good quality (default)
- 18 = High quality
- 28 = Medium quality
## Examples
# Image thumbnail
%Dimension{
name: "thumbnail",
width: 150,
height: 150,
quality: 85,
format: "jpg",
applies_to: "image",
enabled: true,
order: 1
}
# Video 720p variant
%Dimension{
name: "720p",
width: 1280,
height: 720,
quality: 23,
format: "mp4",
applies_to: "video",
enabled: true,
order: 2
}
# Responsive image (maintain aspect ratio)
%Dimension{
name: "large",
width: 1920,
height: nil, # Maintain aspect ratio
quality: 85,
format: nil, # Preserve original
applies_to: "image",
enabled: true,
order: 3
}
"""
use Ecto.Schema
use PhoenixKit.SchemaPrefix
import Ecto.Changeset
@primary_key {:uuid, UUIDv7, autogenerate: true}
@foreign_key_type UUIDv7
# The sizes every variant set has (V205): a size's name is part of every
# file URL, and core and modules ask for these by name. The first three
# of `@aspect_slots` feed grids and justified layouts, so they keep the
# aspect ratio; only `thumbnail` may be cropped.
@standard_slots ~w(thumbnail small medium large video_thumbnail)
@aspect_slots ~w(small medium large)
@type t :: %__MODULE__{
uuid: UUIDv7.t() | nil,
name: String.t() | nil,
width: integer() | nil,
height: integer() | nil,
quality: integer() | nil,
format: String.t() | nil,
applies_to: String.t() | nil,
enabled: boolean(),
maintain_aspect_ratio: boolean(),
alternative_formats: [String.t()],
order: integer(),
variant_set_uuid: UUIDv7.t() | nil,
inserted_at: DateTime.t() | nil,
updated_at: DateTime.t() | nil
}
schema "phoenix_kit_storage_dimensions" do
field :name, :string
field :width, :integer
field :height, :integer
field :quality, :integer
field :format, :string
field :applies_to, :string
field :enabled, :boolean, default: true
field :maintain_aspect_ratio, :boolean, default: true
field :alternative_formats, {:array, :string}, default: []
field :order, :integer, default: 0
# The variant set the size belongs to (V205). The column defaults to the
# Default set, so a writer that names none lands there;
# `read_after_writes` reads the default back.
field :variant_set_uuid, UUIDv7, read_after_writes: true
timestamps(type: :utc_datetime)
end
@doc """
Changeset for creating or updating a dimension.
## Required Fields
- `name`
- `applies_to`
- `width` - Required when `maintain_aspect_ratio` is true or false
## Optional Fields
- `height` - Required when `maintain_aspect_ratio` is false (fixed dimensions)
- `maintain_aspect_ratio` - Whether to preserve aspect ratio (default: true)
## Validation Rules
- Name must be unique
- Width must be specified (always required)
- Height must be specified when `maintain_aspect_ratio` is false
- Width and height must be positive integers
- Quality must be between 1-100 for images or 0-51 for videos
- Applies to must be one of: "image", "video", "both"
- Format must be valid if specified
- Order must be >= 0
"""
def changeset(dimension, attrs) do
dimension
|> cast(attrs, [
:name,
:width,
:height,
:quality,
:format,
:applies_to,
:enabled,
:maintain_aspect_ratio,
:alternative_formats,
:order
])
|> validate_required([:name, :applies_to])
|> validate_format(:name, ~r/^[a-z0-9_]+$/,
message: "must contain only lowercase letters, numbers, and underscores"
)
|> validate_length(:name, min: 1, max: 50)
|> validate_inclusion(:applies_to, ["image", "video", "both"])
|> validate_number(:width, greater_than: 0)
|> validate_number(:height, greater_than: 0)
|> validate_number(:order, greater_than_or_equal_to: 0)
|> validate_dimension_size()
|> validate_quality()
|> validate_format()
|> validate_alternative_formats()
|> validate_standard_slot()
|> unique_constraint(:name, name: :phoenix_kit_storage_dimensions_name_index)
end
@doc "The sizes every variant set has; they cannot be renamed or deleted."
def standard_slots, do: @standard_slots
@doc "The standard sizes that always keep the aspect ratio."
def aspect_slots, do: @aspect_slots
@doc "Whether `dimension` is one of the standard sizes."
def standard_slot?(%__MODULE__{name: name}), do: name in @standard_slots
# A standard size keeps its name, and small/medium/large keep the aspect
# ratio, whatever else an admin changes about them.
defp validate_standard_slot(changeset) do
old_name = changeset.data.name
name = get_field(changeset, :name)
cond do
old_name in @standard_slots and name != old_name ->
add_error(changeset, :name, "is a standard size and cannot be renamed")
# Checked when the size is made, renamed, or its aspect setting is
# changed: an install whose `small` was cropped before V205 can still
# switch it off or change its quality without fixing that first.
name in @aspect_slots and get_field(changeset, :maintain_aspect_ratio) == false and
(is_nil(changeset.data.uuid) or Map.has_key?(changeset.changes, :name) or
Map.has_key?(changeset.changes, :maintain_aspect_ratio)) ->
add_error(
changeset,
:maintain_aspect_ratio,
"must stay on for %{name}: only thumbnail may be cropped",
name: name
)
true ->
changeset
end
end
# Validate dimensions based on maintain_aspect_ratio setting
defp validate_dimension_size(changeset) do
width = get_field(changeset, :width)
height = get_field(changeset, :height)
maintain_aspect = get_field(changeset, :maintain_aspect_ratio)
cond do
is_nil(width) ->
add_error(changeset, :width, "width is required")
maintain_aspect == false && is_nil(height) ->
add_error(changeset, :height, "height is required when not maintaining aspect ratio")
true ->
changeset
end
end
# Validate quality based on file type
defp validate_quality(changeset) do
quality = get_field(changeset, :quality)
applies_to = get_field(changeset, :applies_to)
name = get_field(changeset, :name)
format = get_field(changeset, :format)
cond do
is_nil(quality) ->
changeset
# video_thumbnail outputs images, so it uses image quality scale (1-100)
name == "video_thumbnail" and format in ["jpg", "jpeg", "png", "webp", "gif"] ->
validate_number(changeset, :quality,
greater_than_or_equal_to: 1,
less_than_or_equal_to: 100
)
applies_to in ["image", "both"] ->
validate_number(changeset, :quality,
greater_than_or_equal_to: 1,
less_than_or_equal_to: 100
)
applies_to == "video" ->
validate_number(changeset, :quality,
greater_than_or_equal_to: 0,
less_than_or_equal_to: 51
)
true ->
changeset
end
end
# Validate format is supported
defp validate_format(changeset) do
format = get_field(changeset, :format)
applies_to = get_field(changeset, :applies_to)
if is_nil(format) do
changeset
else
valid_formats = get_valid_formats(applies_to)
if format in valid_formats do
changeset
else
add_error(changeset, :format, "must be one of: #{Enum.join(valid_formats, ", ")}")
end
end
end
# Validate alternative_formats: must be valid image formats, no duplicates, exclude primary format
defp validate_alternative_formats(changeset) do
alt_formats = get_field(changeset, :alternative_formats) || []
primary_format = get_field(changeset, :format)
applies_to = get_field(changeset, :applies_to)
# Filter out empty strings (from hidden checkbox input)
alt_formats = Enum.reject(alt_formats, &(&1 == ""))
changeset = put_change(changeset, :alternative_formats, alt_formats)
cond do
alt_formats == [] ->
changeset
applies_to == "video" ->
add_error(changeset, :alternative_formats, "not supported for video-only dimensions")
true ->
valid = get_valid_image_formats()
invalid = Enum.reject(alt_formats, &(&1 in valid))
has_primary = primary_format && primary_format in alt_formats
has_dupes = length(alt_formats) != length(Enum.uniq(alt_formats))
changeset
|> then(fn cs ->
if invalid != [],
do:
add_error(cs, :alternative_formats, "invalid formats: #{Enum.join(invalid, ", ")}"),
else: cs
end)
|> then(fn cs ->
if has_primary,
do: add_error(cs, :alternative_formats, "must not include the primary format"),
else: cs
end)
|> then(fn cs ->
if has_dupes,
do: add_error(cs, :alternative_formats, "must not contain duplicates"),
else: cs
end)
end
end
defp get_valid_image_formats, do: ["jpg", "jpeg", "png", "webp", "avif", "gif"]
defp get_valid_formats("image"), do: ["jpg", "jpeg", "png", "webp", "gif"]
# Videos can output video formats, but video_thumbnail outputs JPG images
defp get_valid_formats("video"), do: ["mp4", "webm", "avi", "mov", "jpg", "jpeg", "png", "webp"]
defp get_valid_formats("both"),
do: ["jpg", "jpeg", "png", "webp", "gif", "mp4", "webm", "avi", "mov"]
@doc """
Returns whether this dimension applies to images.
"""
def applies_to_images?(%__MODULE__{applies_to: applies_to})
when applies_to in ["image", "both"],
do: true
def applies_to_images?(_), do: false
@doc """
Returns whether this dimension applies to videos.
"""
def applies_to_videos?(%__MODULE__{applies_to: applies_to})
when applies_to in ["video", "both"],
do: true
def applies_to_videos?(_), do: false
@doc """
Returns whether this dimension preserves aspect ratio.
"""
def preserve_aspect_ratio?(%__MODULE__{maintain_aspect_ratio: maintain_aspect}) do
maintain_aspect == true
end
@doc """
Returns a human-readable description of this dimension.
"""
def description(%__MODULE__{
name: name,
width: width,
height: height,
format: format,
maintain_aspect_ratio: maintain_aspect
}) do
size_desc =
cond do
maintain_aspect && width -> "#{width}px wide (aspect ratio maintained)"
width && height -> "#{width}x#{height}"
width -> "#{width}px wide"
height -> "#{height}px tall"
true -> "auto"
end
format_desc = if format, do: " (#{format})", else: ""
"#{name}: #{size_desc}#{format_desc}"
end
end