ActiveStorage - 上传后获取图像尺寸

Bog*_*lan 6 ruby-on-rails rails-activestorage

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

这是我从各种 API 文档中拼凑出来的,但最终还是出现了这个错误:private method 'open' called for #<String:0x00007f9480610118>. 更换blobimage.file的原因轨登录“跳绳图像分析,因为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
Run Code Online (Sandbox Code Playgroud)

这种方法也有问题,因为after_commit它也被称为销毁。

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

equ*_*nt8 19

Rails 内置解决方案

根据ActiveStorage 概述指南,已经有一个现有的解决方案image.file.analyzeimage.file.analyze_later文档)它使用ActiveStorage::Analyzer::ImageAnalyzer

根据#analyze 文档

当新的 blob 第一次附加时,会通过analyze_later 自动进行异步分析。

这意味着您可以使用以下命令访问图像尺寸

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

image.file.metadata['width']
image.file.metadata['height']
Run Code Online (Sandbox Code Playgroud)

所以你的模型可能看起来像:

class Image < ApplicationRecord
  has_one_attached :file

  def height
    file.metadata['height']
  end

  def width
    file.metadata['width']
  end
end
Run Code Online (Sandbox Code Playgroud)

对于 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}
Run Code Online (Sandbox Code Playgroud)

这意味着如果您需要实时访问宽度/高度(例如新上传文件尺寸的 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
Run Code Online (Sandbox Code Playgroud)

注意:在作业中异步完成这是有充分理由的。由于需要执行额外的代码,您的请求的响应会稍微慢一些。所以你需要有一个充分的理由“立即保存尺寸”

该解决方案的镜像可以在How to store Image Width Height in Rails ActiveStorage中找到



DIY解决方案

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

需要更新附件的型号

博格丹·巴兰的解决方案将会起作用。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
Run Code Online (Sandbox Code Playgroud)

update_columns 文档

不需要更新附件的型号

您可能正在创建一个模型,您希望在其中存储文件附件并且不再更新它。(因此,如果您需要更新附件,只需创建新的模型记录并删除旧的)

在这种情况下,代码就更巧妙了:

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
Run Code Online (Sandbox Code Playgroud)

您可能想在保存之前验证附件是否存在。您可以使用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
Run Code Online (Sandbox Code Playgroud)

测试

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
Run Code Online (Sandbox Code Playgroud)

将Active Storange 附加到 Factory Bot 的FilesTestHelper.jpg文章中解释了如何工作