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.jpg
fonctionnement deFilesTestHelper.jpg
est 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é.