【问题标题】:ActiveStorage - get image dimensions after uploadActiveStorage - 上传后获取图像尺寸
【发布时间】:2020-05-24 14:55:30
【问题描述】:

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

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

代码:

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

这种方法也有问题,因为 after_commit 在销毁时也被调用。

TLDR:上传后是否有“正确”的方式来立即获取图像元数据?

【问题讨论】:

    标签: ruby-on-rails rails-activestorage


    【解决方案1】:

    Rails 内置解决方案

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

    根据#analyze docs

    第一次附加新的 blob 时,会通过 analyze_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
    

    FilesTestHelper.jpg 的工作原理在文章 attaching Active Storange to Factory Bot 中进行了解释

    【讨论】:

    • @bbalan 我刚刚发现 Rails 解决方案中的内置功能已经自动执行此操作 (image.file.analyze) 我正在使用 Vanilla Rails 解决方案更新我的回复
    • 一项改进是仅在访问宽度或高度时触发同步分析(如果尚未分析)。
    • 好点@Felix类似于def height; save_dimensions_now if file.meta[:height].nil?; file.meta[:height]; end
    • @equivalent8 是的。然后,如果它需要是生产代码,则应检查作业计划的存在和/或取消作业。
    • @equivalent8 我得到ActiveStorage::FileNotFoundError - ActiveStorage::FileNotFoundError: app/models/doc.rb:86:in save_dimensions_now'. But if I don't use after_commit :save_dimensions_now` 没有存储高度和宽度。导轨 6.0。存储到 AWS。 Postgres。
    【解决方案2】:

    回答自己的问题:我原来的解决方案很接近,但需要安装 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! 上的回调,从而防止了无限循环。

    【讨论】:

    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 2012-02-11
      • 1970-01-01
      • 1970-01-01
      • 2014-12-10
      • 1970-01-01
      • 2011-08-03
      • 1970-01-01
      • 2019-03-11
      • 1970-01-01
      相关资源
      最近更新 更多