ActiveStorage-上传后获取图像尺寸

时间:2020-02-08 20:32:07

标签: ruby-on-rails rails-activestorage

我正在使用Rails + ActiveStorage上传图像文件,并希望在上传后在数据库中保存宽度和高度。但是,我在任何地方都找不到此类示例。

这是我从各种API文档中整理而来的内容,但最终出现此错误:0 3 0 。将private method 'open' called for #<String:0x00007f9480610118>替换为blob会导致Rails记录“正在跳过图像分析,因为ImageMagick不支持该文件”(https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/analyzer/image_analyzer.rb#L39)。

代码:

image.file

此方法也有问题,因为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 也被称为destroy。

TLDR:是否有“适当”的方式在上传后立即获取图像元数据?

3 个答案:

答案 0 :(得分:4)

回答自己的问题:我的原始解决方案已经结束,但是需要安装ImageMagick(不是,并且错误消息没有指出这一点)。这是我的最终代码:

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

我还使用this technique跳过了save!上的回调,以防止无限循环。

答案 1 :(得分:1)

内置解决方案的轨道

根据ActiveStorage Overview Guild,已经存在使用docs的解决方案image.file.analyzeimage.file.analyze_laterActiveStorage::Analyzer::ImageAnalyzer

根据#analyze docs

首次连接新的Blob时,会通过analyst_later自动进行异步分析。

这意味着您可以通过以下方式访问图像尺寸

image.file.metadata
#=> {"identified"=>true, "width"=>2448, "height"=>3264, "analyzed"=>true}

image.file.metadata['width']
image.file.metadata['height']

因此您的模型可以如下所示:

class Image < ApplicationRecord
  has_one_attached :file

  def height
    file.metadata['height']
  end

  def width
    file.metadata['width']
  end
end

在90%的常规情况下,您对此都很满意

但是:问题在于,这是“异步分析”(#analyze_later),这意味着上传后不会立即存储元数据

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}

这意味着如果您需要实时访问宽度/高度(例如,新上传文件的尺寸的API响应)

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

注意:在Job中异步完成此操作有充分的理由。由于需要执行额外的代码,因此您的请求响应速度会稍慢。因此,您需要有充分的理由“立即保存尺寸”

此解决方案的镜像位于How to store Image Width Height in Rails ActiveStorage



DIY解决方案

建议:不要这样做,要依靠现有的Vanilla Rails解决方案

需要更新附件的模型

Bogdan Balan's solution将起作用。这是没有skip_set_dimensions attr_accessor

的相同解决方案的重写
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

update_columns docs

不需要更新附件的模型

可能是您正在创建一个想要在其中存储文件附件的模型,而不再对其进行更新。 (因此,如果您需要更新附件,只需创建新的模型记录并删除旧的模型记录即可)

在这种情况下,代码甚至更加流畅:

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

您是否有机会在保存之前验证附件是否存在。您可以使用active_storage_validations gem

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
测试
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

attaching Active Storange to Factory Bot

中说明了FilesTestHelper.jpg的工作方式

答案 2 :(得分:-1)

我认为您可以在更新之前从javascript中获取尺寸,然后将这些数据发布到控制器中。 您可以查看一下: Check image width and height before upload with Javascript

相关问题