3
votes

ActiveStorage - obtenir les dimensions de l'image après le téléchargement

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?


0 commentaires

3 Réponses :


-1
votes

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


0 commentaires

4
votes

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.


2 commentaires

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



1
votes

Solution intégrée de rails

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



Solution de bricolage

recommandation: ne le faites pas, comptez sur la solution Vanilla Rails existante

Modèles qui doivent mettre à jour la pièce jointe

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

documentation update_columns

Modèles qui n'ont pas besoin de mettre à jour la pièce jointe

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 de FilesTestHelper.jpg est expliqué dans l'article attachant Active Storange à Factory Bot


5 commentaires

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é.