Skip to content

Commit

Permalink
Merge pull request #294 from mohammednasser-32/allow_multiple_aspect_…
Browse files Browse the repository at this point in the history
…ratios

Allow multiple aspect ratios
  • Loading branch information
Mth0158 authored Dec 21, 2024
2 parents f96c59c + 98cfe67 commit 0010ecb
Show file tree
Hide file tree
Showing 22 changed files with 242 additions and 121 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ en:
aspect_ratio_not_landscape: "must be a landscape image"
aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
image_not_processable: "is not a valid image"
aspect_ratio_invalid: "has invalid aspect ratio"
```
In several cases, Active Storage Validations provides variables to help you customize messages:
Expand Down
1 change: 1 addition & 0 deletions config/locales/da.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ da:
aspect_ratio_not_landscape: "skal være et landskabsbillede"
aspect_ratio_is_not: "skal have et størrelsesforhold på %{aspect_ratio}"
image_not_processable: "er ikke et gyldigt billede"
aspect_ratio_invalid: "har et ugyldigt billedformat"
1 change: 1 addition & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ de:
aspect_ratio_not_landscape: "muss Querformat sein"
aspect_ratio_is_not: "muss ein Bildseitenverhältnis von %{aspect_ratio} haben"
image_not_processable: "ist kein gültiges Bild"
aspect_ratio_invalid: "hat ein ungültiges Seitenverhältnis"
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ en:
aspect_ratio_not_landscape: "must be a landscape image"
aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
image_not_processable: "is not a valid image"
aspect_ratio_invalid: "has an invalid aspect ratio"
1 change: 1 addition & 0 deletions config/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ es:
aspect_ratio_not_landscape: "debe ser una imagen apaisada"
aspect_ratio_is_not: "debe tener una relación de aspecto de %{aspect_ratio}"
image_not_processable: "no es una imagen válida"
aspect_ratio_invalid: "tiene una relación de aspecto no válida"
1 change: 1 addition & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ fr:
aspect_ratio_not_landscape: "doit être une image en format paysage"
aspect_ratio_is_not: "doit avoir un rapport hauteur / largeur de %{aspect_ratio}"
image_not_processable: "n'est pas une image valide"
aspect_ratio_invalid: "a un rapport hauteur / largeur invalide"
1 change: 1 addition & 0 deletions config/locales/it.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ it:
aspect_ratio_not_landscape: "l’orientamento dell’immagine deve essere orizzontale"
aspect_ratio_is_not: "deve avere un rapporto altezza / larghezza di %{aspect_ratio}"
image_not_processable: "non è un'immagine valida"
aspect_ratio_invalid: "ha proporzioni non valide"
1 change: 1 addition & 0 deletions config/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ ja:
aspect_ratio_not_landscape: "は横長にしてください"
aspect_ratio_is_not: "のアスペクト比は %{aspect_ratio} にしてください"
image_not_processable: "は不正な画像です"
aspect_ratio_invalid: "アスペクト比が無効です"
1 change: 1 addition & 0 deletions config/locales/nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ nl:
aspect_ratio_not_landscape: "moet een liggende afbeelding zijn"
aspect_ratio_is_not: "moet een beeldverhouding hebben van %{aspect_ratio}"
image_not_processable: "is geen geldige afbeelding"
aspect_ratio_invalid: "heeft een ongeldige beeldverhouding"
1 change: 1 addition & 0 deletions config/locales/pl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ pl:
aspect_ratio_not_landscape: "musi mieć proporcje pejzażu"
aspect_ratio_is_not: "musi mieć proporcje %{aspect_ratio}"
image_not_processable: "nie jest prawidłowym obrazem"
aspect_ratio_invalid: "ma nieprawidłowy współczynnik proporcji"
1 change: 1 addition & 0 deletions config/locales/pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ pt-BR:
aspect_ratio_not_landscape: "não está no formato paisagem"
aspect_ratio_is_not: "não contém uma proporção de %{aspect_ratio}"
image_not_processable: "não é uma imagem válida"
aspect_ratio_invalid: "tem uma proporção de aspecto inválida"
1 change: 1 addition & 0 deletions config/locales/ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ ru:
aspect_ratio_not_landscape: "должно быть пейзажное изображение"
aspect_ratio_is_not: "должен иметь соотношение сторон %{aspect_ratio}"
image_not_processable: "не является допустимым изображением"
aspect_ratio_invalid: "имеет недопустимое соотношение сторон"
1 change: 1 addition & 0 deletions config/locales/sv.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ sv:
aspect_ratio_not_landscape: "måste vara en landskapsorienterad bild"
aspect_ratio_is_not: "måste ha en följande aspect ratio %{aspect_ratio}"
image_not_processable: "är inte en giltig bild"
aspect_ratio_invalid: "har ett ogiltigt bildförhållande"
1 change: 1 addition & 0 deletions config/locales/tr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ tr:
aspect_ratio_not_landscape: "yatay bir imaj olmalı"
aspect_ratio_is_not: "%{aspect_ratio} en boy oranına sahip olmalı"
image_not_processable: "geçerli bir imaj değil"
aspect_ratio_invalid: "geçersiz bir en boy oranına sahip"
1 change: 1 addition & 0 deletions config/locales/uk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ uk:
aspect_ratio_not_landscape: "мусить бути пейзажне зображення"
aspect_ratio_is_not: "мусить мати співвідношення сторін %{aspect_ratio}"
image_not_processable: "не є допустимим зображенням"
aspect_ratio_invalid: "має недійсне співвідношення сторін"
1 change: 1 addition & 0 deletions config/locales/vi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ vi:
aspect_ratio_not_landscape: "phải là ảnh ngang"
aspect_ratio_is_not: "phải có tỉ lệ ảnh %{aspect_ratio}"
image_not_processable: "không phải là ảnh"
aspect_ratio_invalid: "có tỷ lệ khung hình không hợp lệ"
1 change: 1 addition & 0 deletions config/locales/zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ zh-CN:
aspect_ratio_not_landscape: "必须是横屏图片"
aspect_ratio_is_not: "纵横比必须是 %{aspect_ratio}"
image_not_processable: "不是有效的图像"
aspect_ratio_invalid: "宽高比无效"
270 changes: 150 additions & 120 deletions lib/active_storage_validations/aspect_ratio_validator.rb
Original file line number Diff line number Diff line change
@@ -1,120 +1,150 @@
# frozen_string_literal: true

require_relative 'shared/asv_active_storageable'
require_relative 'shared/asv_analyzable'
require_relative 'shared/asv_attachable'
require_relative 'shared/asv_errorable'
require_relative 'shared/asv_optionable'
require_relative 'shared/asv_symbolizable'

module ActiveStorageValidations
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
include ASVActiveStorageable
include ASVAnalyzable
include ASVAttachable
include ASVErrorable
include ASVOptionable
include ASVSymbolizable

AVAILABLE_CHECKS = %i[with].freeze
NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
ERROR_TYPES = %i[
image_metadata_missing
aspect_ratio_not_square
aspect_ratio_not_portrait
aspect_ratio_not_landscape
aspect_ratio_is_not
].freeze
PRECISION = 3.freeze

def check_validity!
ensure_at_least_one_validator_option
ensure_aspect_ratio_validity
end

def validate_each(record, attribute, _value)
return if no_attachments?(record, attribute)

validate_changed_files_from_metadata(record, attribute)
end

private

def is_valid?(record, attribute, attachable, metadata)
flat_options = set_flat_options(record)

return if image_metadata_missing?(record, attribute, attachable, flat_options, metadata)

case flat_options[:with]
when :square then validate_square_aspect_ratio(record, attribute, attachable, flat_options, metadata)
when :portrait then validate_portrait_aspect_ratio(record, attribute, attachable, flat_options, metadata)
when :landscape then validate_landscape_aspect_ratio(record, attribute, attachable, flat_options, metadata)
when ASPECT_RATIO_REGEX then validate_regex_aspect_ratio(record, attribute, attachable, flat_options, metadata)
end
end

def image_metadata_missing?(record, attribute, attachable, flat_options, metadata)
return false if metadata.present? && metadata[:width].to_i > 0 && metadata[:height].to_i > 0

errors_options = initialize_error_options(options, attachable)
errors_options[:aspect_ratio] = flat_options[:with]
add_error(record, attribute, :image_metadata_missing, **errors_options)
true
end

def validate_square_aspect_ratio(record, attribute, attachable, flat_options, metadata)
return if metadata[:width] == metadata[:height]

errors_options = initialize_error_options(options, attachable)
errors_options[:aspect_ratio] = flat_options[:with]
add_error(record, attribute, :aspect_ratio_not_square, **errors_options)
end

def validate_portrait_aspect_ratio(record, attribute, attachable, flat_options, metadata)
return if metadata[:width] < metadata[:height]

errors_options = initialize_error_options(options, attachable)
errors_options[:aspect_ratio] = flat_options[:with]
add_error(record, attribute, :aspect_ratio_not_portrait, **errors_options)
end

def validate_landscape_aspect_ratio(record, attribute, attachable, flat_options, metadata)
return if metadata[:width] > metadata[:height]

errors_options = initialize_error_options(options, attachable)
errors_options[:aspect_ratio] = flat_options[:with]
add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
end

def validate_regex_aspect_ratio(record, attribute, attachable, flat_options, metadata)
flat_options[:with] =~ ASPECT_RATIO_REGEX
x = $1.to_i
y = $2.to_i

return if x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)

errors_options = initialize_error_options(options, attachable)
errors_options[:aspect_ratio] = "#{x}:#{y}"
add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
end

def ensure_at_least_one_validator_option
unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
raise ArgumentError, 'You must pass :with to the validator'
end
end

def ensure_aspect_ratio_validity
return true if options[:with]&.is_a?(Proc)

unless NAMED_ASPECT_RATIOS.include?(options[:with]) || options[:with] =~ ASPECT_RATIO_REGEX
raise ArgumentError, <<~ERROR_MESSAGE
You must pass a valid aspect ratio to the validator
It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
ERROR_MESSAGE
end
end
end
end
# frozen_string_literal: true

require_relative 'shared/asv_active_storageable'
require_relative 'shared/asv_analyzable'
require_relative 'shared/asv_attachable'
require_relative 'shared/asv_errorable'
require_relative 'shared/asv_optionable'
require_relative 'shared/asv_symbolizable'

module ActiveStorageValidations
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
include ASVActiveStorageable
include ASVAnalyzable
include ASVAttachable
include ASVErrorable
include ASVOptionable
include ASVSymbolizable

AVAILABLE_CHECKS = %i[with in].freeze
NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
ERROR_TYPES = %i[
aspect_ratio_not_square
aspect_ratio_not_portrait
aspect_ratio_not_landscape
aspect_ratio_is_not
aspect_ratio_invalid
image_metadata_missing
].freeze
PRECISION = 3.freeze

def check_validity!
ensure_at_least_one_validator_option
ensure_aspect_ratio_validity
end

def validate_each(record, attribute, _value)
return if no_attachments?(record, attribute)

flat_options = set_flat_options(record)
@authorized_aspect_ratios = authorized_aspect_ratios_from_options(flat_options).compact
return if @authorized_aspect_ratios.empty?

validate_changed_files_from_metadata(record, attribute)
end

private

def is_valid?(record, attribute, attachable, metadata)
!image_metadata_missing?(record, attribute, attachable, metadata) &&
authorized_aspect_ratio?(record, attribute, attachable, metadata)
end

def authorized_aspect_ratio?(record, attribute, attachable, metadata)
attachable_aspect_ratio_is_authorized = @authorized_aspect_ratios.any? do |authorized_aspect_ratio|
case authorized_aspect_ratio
when :square then valid_square_aspect_ratio?(metadata)
when :portrait then valid_portrait_aspect_ratio?(metadata)
when :landscape then valid_landscape_aspect_ratio?(metadata)
when ASPECT_RATIO_REGEX then valid_regex_aspect_ratio?(authorized_aspect_ratio, metadata)
end
end

return true if attachable_aspect_ratio_is_authorized

errors_options = initialize_error_options(options, attachable)
errors_options[:aspect_ratio] = string_aspect_ratios
add_error(record, attribute, aspect_ratio_error_mapping, **errors_options)
false
end

def aspect_ratio_error_mapping
return :aspect_ratio_invalid unless @authorized_aspect_ratios.one?

aspect_ratio = @authorized_aspect_ratios.first
NAMED_ASPECT_RATIOS.include?(aspect_ratio) ? :"aspect_ratio_not_#{aspect_ratio}" : :aspect_ratio_is_not
end

def image_metadata_missing?(record, attribute, attachable, metadata)
return false if metadata[:width].to_i > 0 && metadata[:height].to_i > 0

errors_options = initialize_error_options(options, attachable)
errors_options[:aspect_ratio] = string_aspect_ratios
add_error(record, attribute, :image_metadata_missing, **errors_options)
true
end

def valid_square_aspect_ratio?(metadata)
metadata[:width] == metadata[:height]
end

def valid_portrait_aspect_ratio?(metadata)
metadata[:width] < metadata[:height]
end

def valid_landscape_aspect_ratio?(metadata)
metadata[:width] > metadata[:height]
end

def valid_regex_aspect_ratio?(aspect_ratio, metadata)
aspect_ratio =~ ASPECT_RATIO_REGEX
x = ::Regexp.last_match(1).to_i
y = ::Regexp.last_match(2).to_i

x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
end

def ensure_at_least_one_validator_option
return if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }

raise ArgumentError, 'You must pass either :with or :in to the validator'
end

def ensure_aspect_ratio_validity
return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)

authorized_aspect_ratios_from_options(options).each do |aspect_ratio|
unless NAMED_ASPECT_RATIOS.include?(aspect_ratio) || aspect_ratio =~ ASPECT_RATIO_REGEX
raise ArgumentError, invalid_aspect_ratio_message
end
end
end

def invalid_aspect_ratio_message
<<~ERROR_MESSAGE
You must pass a valid aspect ratio to the validator
It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
ERROR_MESSAGE
end

def authorized_aspect_ratios_from_options(flat_options)
(Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in]))
end

def string_aspect_ratios
@authorized_aspect_ratios.map do |aspect_ratio|
if NAMED_ASPECT_RATIOS.include?(aspect_ratio)
aspect_ratio
else
aspect_ratio =~ ASPECT_RATIO_REGEX
x = ::Regexp.last_match(1).to_i
y = ::Regexp.last_match(2).to_i

"#{x}:#{y}"
end
end.join(', ')
end
end
end
Loading

0 comments on commit 0010ecb

Please sign in to comment.