|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module ActiveStorageValidations |
| 4 | + # = Active Storage Image \Analyzer |
| 5 | + # |
| 6 | + # This is an abstract base class for image analyzers, which extract width and height from an image attachable. |
| 7 | + # |
| 8 | + # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience. |
| 9 | + # |
| 10 | + # Example: |
| 11 | + # |
| 12 | + # ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(attachable).metadata |
| 13 | + # # => { width: 4104, height: 2736 } |
| 14 | + class Analyzer::ImageAnalyzer < Analyzer |
| 15 | + def self.accept?(attachable) |
| 16 | + image?(attachable) |
| 17 | + end |
| 18 | + |
| 19 | + def metadata |
| 20 | + read_image do |image| |
| 21 | + if rotated_image?(image) |
| 22 | + { width: image.height, height: image.width } |
| 23 | + else |
| 24 | + { width: image.width, height: image.height } |
| 25 | + end |
| 26 | + end |
| 27 | + end |
| 28 | + |
| 29 | + private |
| 30 | + |
| 31 | + def image |
| 32 | + case @attachable |
| 33 | + when ActiveStorage::Blob, String |
| 34 | + blob = @attachable.is_a?(String) ? ActiveStorage::Blob.find_signed!(@attachable) : @attachable |
| 35 | + tempfile = tempfile_from_blob(blob) |
| 36 | + |
| 37 | + image_from_path(tempfile.path) |
| 38 | + when Hash |
| 39 | + io = @attachable[:io] |
| 40 | + file = io.is_a?(StringIO) ? tempfile_from_io(io) : File.open(io) |
| 41 | + |
| 42 | + image_from_path(file.path) |
| 43 | + when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile |
| 44 | + image_from_path(@attachable.path) |
| 45 | + when File |
| 46 | + supports_file_attachment? ? image_from_path(@attachable.path) : raise_rails_like_error(@attachable) |
| 47 | + when Pathname |
| 48 | + supports_pathname_attachment? ? image_from_path(@attachable.to_s) : raise_rails_like_error(@attachable) |
| 49 | + else |
| 50 | + raise_rails_like_error(@attachable) |
| 51 | + end |
| 52 | + end |
| 53 | + |
| 54 | + def tempfile_from_blob(blob) |
| 55 | + tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter], binmode: true) |
| 56 | + |
| 57 | + blob.download { |chunk| tempfile.write(chunk) } |
| 58 | + |
| 59 | + tempfile.flush |
| 60 | + tempfile.rewind |
| 61 | + tempfile |
| 62 | + end |
| 63 | + |
| 64 | + def tempfile_from_io(io) |
| 65 | + tempfile = Tempfile.new([File.basename(@attachable[:filename], '.*'), File.extname(@attachable[:filename])], binmode: true) |
| 66 | + |
| 67 | + IO.copy_stream(io, tempfile) |
| 68 | + io.rewind |
| 69 | + |
| 70 | + tempfile.flush |
| 71 | + tempfile.rewind |
| 72 | + tempfile |
| 73 | + end |
| 74 | + |
| 75 | + def image_from_path(path) |
| 76 | + raise NotImplementedError |
| 77 | + end |
| 78 | + |
| 79 | + def rotated_image?(image) |
| 80 | + raise NotImplementedError |
| 81 | + end |
| 82 | + end |
| 83 | +end |
0 commit comments