ActiveStorage - 上传后获取图像尺寸
ActiveStorage - get image dimensions after upload
我正在使用Rails + ActiveStorage上传图片文件,上传后想把宽高保存在数据库中。但是,我无法在任何地方找到任何此类示例。
这是我从各种 API 文档中拼凑出来的内容,但最终出现了这个错误:private method 'open' called for #<String:0x00007f9480610118>
。将 blob
替换为 image.file
会导致 rails 记录 "Skipping image analysis because ImageMagick doesn't support the file" (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:上传后是否有"proper"立即获取图像元数据的方法?
我认为您可以在更新前从 javascript 获取维度,然后 post 这些数据进入控制器。
你可以看看:
Check image width and height before upload with Javascript
回答自己的问题:我原来的解决方案很接近,但需要安装 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!
上的回调,防止无限循环。
Rails 内置解决方案
根据ActiveStorage Overview Guild there is already existing solution image.file.analyze
and image.file.analyze_later
(docs ) which uses ActiveStorage::Analyzer::ImageAnalyzer
New blobs are automatically and asynchronously analyzed via analyze_later when they're attached for the first time.
这意味着您可以使用
访问您的图像尺寸
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}
这意味着如果您需要实时访问 width/height(例如 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
Note: there is a good reason why this is done async in a Job. Responses of your request will be slightly slower due to this extra code execution needs to happen. So you need to have a good reason to "save dimensions now"
Mirror of this solution can be found at How to store Image Width Height in Rails ActiveStorage
DIY 解决方案
recommendation: don't do it, rely on existing Vanilla Rails 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
不需要更新附件的机型
您可能正在创建要存储文件附件并且永远不再更新它的模型。 (因此,如果您需要更新附件,您只需创建新的模型记录并删除旧的)
在那种情况下,代码甚至更流畅:
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
How FilesTestHelper.jpg
work is explained in article attaching Active Storange to Factory Bot
我正在使用Rails + ActiveStorage上传图片文件,上传后想把宽高保存在数据库中。但是,我无法在任何地方找到任何此类示例。
这是我从各种 API 文档中拼凑出来的内容,但最终出现了这个错误:private method 'open' called for #<String:0x00007f9480610118>
。将 blob
替换为 image.file
会导致 rails 记录 "Skipping image analysis because ImageMagick doesn't support the file" (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:上传后是否有"proper"立即获取图像元数据的方法?
我认为您可以在更新前从 javascript 获取维度,然后 post 这些数据进入控制器。 你可以看看: Check image width and height before upload with Javascript
回答自己的问题:我原来的解决方案很接近,但需要安装 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!
上的回调,防止无限循环。
Rails 内置解决方案
根据ActiveStorage Overview Guild there is already existing solution image.file.analyze
and image.file.analyze_later
(docs ) which uses ActiveStorage::Analyzer::ImageAnalyzer
New blobs are automatically and asynchronously analyzed via analyze_later when they're attached for the first time.
这意味着您可以使用
访问您的图像尺寸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}
这意味着如果您需要实时访问 width/height(例如 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
Note: there is a good reason why this is done async in a Job. Responses of your request will be slightly slower due to this extra code execution needs to happen. So you need to have a good reason to "save dimensions now"
Mirror of this solution can be found at How to store Image Width Height in Rails ActiveStorage
DIY 解决方案
recommendation: don't do it, rely on existing Vanilla Rails 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
不需要更新附件的机型
您可能正在创建要存储文件附件并且永远不再更新它的模型。 (因此,如果您需要更新附件,您只需创建新的模型记录并删除旧的)
在那种情况下,代码甚至更流畅:
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
How
FilesTestHelper.jpg
work is explained in article attaching Active Storange to Factory Bot