J'utilise Rails + ActiveStorage pour télécharger des fichiers image et je souhaite enregistrer la largeur et la hauteur dans la base de données après le téléchargement. Cependant, j'ai du mal à trouver des exemples de cela n'importe où.
C'est ce que j'ai bricolé à partir de divers documents d'API, mais je me retrouve avec cette erreur: private method 'open' called for #<String:0x00007f9480610118> . Le remplacement de l'objet blob par image.file amène les rails à enregistrer "Sauter l'analyse d'image car ImageMagick ne prend pas en charge le fichier" ( https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/analyzer/image_analyzer. rb # L39 ).
Code:
class Image < ApplicationRecord
after_commit { |image| set_dimensions image }
has_one_attached :file
def set_dimensions(image)
if (image.file.attached?)
blob = image.file.download
# error: private method `open' called for #<String:0x00007f9480610118>
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
end
end
end
Cette approche est également problématique car after_commit est également appelé sur destroy.
TLDR: Existe - t-il un moyen "approprié" d'obtenir les métadonnées d'image immédiatement après le téléchargement?
3 Réponses :
Je pense que vous pouvez obtenir la dimension de javascript avant la mise à jour, puis publier ces données dans le contrôleur. vous pouvez le vérifier: vérifiez la largeur et la hauteur de l'image avant de la télécharger avec Javascript
Répondre à ma propre question: ma solution d'origine était proche, mais nécessitait l'installation d'ImageMagick (ce n'était pas le cas, et les messages d'erreur ne le signalaient pas). C'était mon code final:
class Image < ApplicationRecord
attr_accessor :skip_set_dimensions
after_commit ({unless: :skip_set_dimensions}) { |image| set_dimensions image }
has_one_attached :file
def set_dimensions(image)
if (Image.exists?(image.id))
if (image.file.attached?)
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(image.file).metadata
image.width = meta[:width]
image.height = meta[:height]
else
image.width = 0
image.height = 0
end
image.skip_set_dimensions = true
image.save!
end
end
end
J'ai également utilisé cette technique pour ignorer le rappel lors de l' save! , empêchant une boucle infinie.
api.rubyonrails.org/classes/ActiveStorage/Analyzer/...
Votre solution fonctionnera, mais pour éviter les skip_set_dimensions vous pouvez simplement utiliser: update_columns(width: meta[:width], height: meta[:height]) car les validations sont ignorées apidock.com/rails/ActiveRecord/Persistence/ update_columns
Selon ActiveStorage Overview Guild, il existe déjà des solutions image.file.analyze et image.file.analyze_later ( docs ) qui utilisent ActiveStorage :: Analyzer :: ImageAnalyzer
Selon la documentation #analyze :
Les nouveaux objets blob sont analysés automatiquement et de manière asynchrone via analy_later lorsqu'ils sont attachés pour la première fois.
Cela signifie que vous pouvez accéder aux dimensions de votre image avec
require 'rails_helper'
RSpec.describe Image, type: :model do
let(:image) { build :image, file: image_file }
context 'when trying to upload jpg' do
let(:image_file) { FilesTestHelper.jpg } # https://blog.eq8.eu/til/factory-bot-trait-for-active-storange-has_attached.html
it do
expect { image.save }.to change { image.height }.from(nil).to(35)
end
it do
expect { image.save }.to change { image.width }.from(nil).to(37)
end
it 'on update it should not cause infinitte loop' do
image.save! # creates
image.rotation = 90 # whatever change, some random property on Image model
image.save! # updates
# no stack ofverflow happens => good
end
end
context 'when trying to upload pdf' do
let(:image_file) { FilesTestHelper.pdf } # https://blog.eq8.eu/til/factory-bot-trait-for-active-storange-has_attached.html
it do
expect { image.save }.not_to change { image.height }
end
end
end
Ainsi, votre modèle peut ressembler à:
class Image < ApplicationRecord
after_commit :set_dimensions, on: :create
has_one_attached :file
# validations by active_storage_validations
validates :file, attached: true,
size: { less_than: 12.megabytes , message: 'image too large' },
content_type: { in: ['image/png', 'image/jpg', 'image/jpeg'], message: 'needs to be an PNG or JPEG image' }
private
def set_dimensions
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(file).metadata
self.height = meta[:height] || 0
self.width = meta[:width] || 0
save!
end
end
Pour 90% des cas réguliers, vous êtes bon avec cela
MAIS: le problème est qu'il s'agit d'une "analyse asynchrone" ( #analyze_later ), ce qui signifie que vous n'aurez pas les métadonnées stockées juste après le téléchargement
class Image < ApplicationRecord
after_commit :set_dimensions, on: :create
has_one_attached :file
private
def set_dimensions
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(file).metadata
self.height = meta[:height] || 0
self.width = meta[:width] || 0
save!
end
end
Cela signifie que si vous avez besoin d'accéder à la largeur / hauteur en temps réel (par exemple, la réponse API des dimensions du fichier fraîchement téléchargé), vous devrez peut-être faire
class Image < ApplicationRecord
after_commit :set_dimensions
has_one_attached :file
private
def set_dimensions
if (file.attached?)
meta = ActiveStorage::Analyzer::ImageAnalyzer.new(file).metadata
height = meta[:height]
width = meta[:width]
else
height = 0
width = 0
end
update_columns(width: width, height: height) # this will save to DB without Rails callbacks
end
end
Remarque: il y a une bonne raison pour laquelle cela est effectué de manière asynchrone dans un Job. Les réponses à votre demande seront légèrement plus lentes en raison de cette exécution de code supplémentaire. Vous devez donc avoir une bonne raison pour "enregistrer les dimensions maintenant"
Le miroir de cette solution peut être trouvé à Comment stocker la hauteur de la largeur de l'image dans les rails ActiveStorage
recommandation: ne le faites pas, comptez sur la solution Vanilla Rails existante
La solution de Bogdan Balan fonctionnera. Voici une réécriture de la même solution sans le skip_set_dimensions attr_accessor
class Image < ApplicationRecord
has_one_attached :file
after_commit :save_dimensions_now
def height
file.metadata['height']
end
def width
file.metadata['width']
end
private
def save_dimensions_now
file.analyze if file.attached?
end
end
Il est fort probable que vous créez un modèle dans lequel vous souhaitez stocker la pièce jointe et ne jamais la mettre à jour à nouveau. (Donc, si vous avez besoin de mettre à jour la pièce jointe, créez simplement un nouvel enregistrement de modèle et supprimez l'ancien)
Dans ce cas, le code est encore plus lisse:
image.save!
image.file.metadata
#=> {"identified"=>true}
image.file.analyzed?
# => nil
# .... after ActiveJob for analyze_later finish
image.reload
image.file.analyzed?
# => true
#=> {"identified"=>true, "width"=>2448, "height"=>3264, "analyzed"=>true}
Vous souhaiterez probablement valider si la pièce jointe est présente avant de l'enregistrer. Vous pouvez utiliser la gemme active_storage_validations
class Image < ApplicationRecord
has_one_attached :file
def height
file.metadata['height']
end
def width
file.metadata['width']
end
end
tester
image.file.metadata
#=> {"identified"=>true, "width"=>2448, "height"=>3264, "analyzed"=>true}
image.file.metadata['width']
image.file.metadata['height']
FilesTestHelper.jpgfonctionnement deFilesTestHelper.jpgest expliqué dans l'article attachant Active Storange à Factory Bot
Très lisse, merci!
@bbalan Je viens de découvrir qu'il existe déjà une solution bulit dans Rails qui le fait déjà automatiquement ( image.file.analyze ) Je mets à jour ma réponse avec la solution Vanilla Rails
Une amélioration serait de ne déclencher l'analyse de synchronisation que lors de l'accès à la largeur ou à la hauteur (si elle n'a pas déjà été analysée).
bon point @Felix quelque chose comme def height; save_dimensions_now if file.meta[:height].nil?; file.meta[:height]; end .
@ équivalent8 ya. Et puis s'il doit s'agir de code de production, l'existence d'un planning pour un travail doit être vérifiée et / ou le travail annulé.