Saving EXIF data to Image model before saving to database (Rails/S3/SuckerPunch)

779 views
Skip to first unread message

yram...@umich.edu

unread,
Jan 7, 2018, 7:45:29 PM1/7/18
to Shrine
Hello!

I've looked all over these threads but am having a hard time finding the help I need regarding writing specific EXIF data to an Image object before saving it to the database. I'm using Rails 5.1.4 with Shrine to upload images to Amazon S3. I also use SuckerPunch to asynchronously perform the promote and delete jobs since I'm allowing multiple file uploads and deletion.

So my problem is that I want to overwrite the created_at attribute so that it corresponds to the original time the photo was taken according to the EXIF metadata. However, my solution for this (below) does not work. Instead, I get the following error:

NoMethodError (undefined method `[]' for #<ImageUploader::UploadedFile:0x007fa12d1def68>):

app/models/photo.rb:7:
in `original_created_at'
app/models/photo.rb:11:
in `overwrite_created_at_date'
app
/controllers/photos_controller.rb:9:in `create'

For reference, this is my app/models/photos.rb file:

class Photo < ApplicationRecord
  belongs_to :album
  before_save :overwrite_created_at_date
  include ImageUploader::Attachment.new(:image)

  def original_created_at
    DateTime.strptime(self.image[:large].data["metadata"]["exif"]["DateTimeOriginal"], "%Y:%m:%d %H:%M:%S")
  end

  def overwrite_created_at_date
    self.created_at = self.original_created_at
  end
end

Ideally, I don't want to use `self.image[:large].data` to access the EXIF data. If there is a better way to do this, please let me know. That seemed to be the best way for me, however.

And this is my app/controllers/photos_controller.rb create method:


#...

def create
  @photo = @album.photos.create(photo_params)
  @photo.album = @album

  if @photo.save
    respond_to do |format|
      format.js { render layout: false, content_type: 'text/javascript' }
    end
  else
    flash.now[:alert] = 'Error while uploading photo.'
  end
end

#...



I'm handling the image uploading with an ImageUploader class. Here is app/models/image_uploader.rb:

require "image_processing/mini_magick"

class ImageUploader < Shrine
  include ImageProcessing::MiniMagick

  plugin :add_metadata
  plugin :remove_attachment
  plugin :validation_helpers
  plugin :pretty_location
  plugin :processing
  plugin :versions

  Attacher.validate do
    validate_mime_type_inclusion ['image/jpeg', 'image/png', 'image/gif']
  end

  add_metadata :exif do |io|
    MiniMagick::Image.new(io.path).exif
  end

  process(:store) do |io, context|
    thumb = resize_to_limit!(io.download, 300, 300)
    small = resize_to_limit!(io.download, 461, 307)
    medium = resize_to_limit!(io.download, 864, 576)
    large = resize_to_limit!(io.download, 1440, 960)

    { original: io, large: large, medium: medium, small: small, thumb: thumb }
  end
end

I don't think I have a clear grasp on how to utilize the ImageUploader class to extract metadata and write it to objects. I have a feeling that may be an easier way to handle this, but please do let me know.

Finally, the reason I want to do all this is so that I can order the photos on a page by the original_created_date (the DateTime in which the photo was taken) instead of the DateTime the object was added to the database. Something like this:

app/controllers/albums_controller.rb:

#...

def show
  @album = Album.find(params[:id])

  # Where created_at corresponds to the DateTime the photo was taken on the camera
  @photos = @album.photos.order("created_at DESC")
end

#...


Please help!
Thanks.

Hiren Mistry

unread,
Jan 8, 2018, 9:31:34 PM1/8/18
to Shrine
Hi

Ok you have a few problems going on. I'll address them one by one.

You're missing determine_mime_type plugin for mime type validation to work
You don't have to extract and store the entire Exif data if you don't need it. In the test script below I simplified it to only extract the created timestamp. There might be another way of doing this without metadata plugin...
(This is not an issue) When processing images I suggest to resize image from largest to smallest in order and use the previous size to generate the next size. Doing this preserves the details, uses less memory, and is faster. This may have changed since I tested it...
Using before_save will overwrite created_at during every save. You probably only want to do this one time on create.

See test script and my comments below. Hope this helps.

Regards
Hiren.

Test script:
require 'active_record'
require "shrine"
require "shrine/storage/file_system"
require "tmpdir"
require "open-uri"
require 'byebug'
require 'mini_magick'
require "image_processing/mini_magick"


# Configure Shrine
Shrine.plugin :activerecord
Shrine.storages = {
  cache
: Shrine::Storage::FileSystem.new(Dir.tmpdir, prefix: "cache"),
  store
: Shrine::Storage::FileSystem.new(Dir.tmpdir, prefix: "store"),
}


# Uploader
class PhotoUploader < Shrine
 
# plugins and uploading logic
  include
ImageProcessing::MiniMagick


  plugin
:add_metadata
 
# plugin :remove_attachment
  plugin
:validation_helpers
  plugin
:pretty_location
 
# plugin :processing
 
# plugin :versions
  plugin
:determine_mime_type  # <===== 1. Missing this plugin for mime type validation to work



 
Attacher.validate do
    validate_mime_type_inclusion
['image/jpeg', 'image/png', 'image/gif']
 
end



 
# 2. You can use your method to extract all Exif data from image and store in the database record
 
#    or only extract needed information i.e. `DateTimeOriginal`
  add_metadata
:original_created_at do |io|
   
MiniMagick::Image.new(io.path).exif["DateTimeOriginal"]
 
end


 
# 3. When processing images I suggest to resize image from largest to smallest in order and
 
#    use the previous size to generate the next size. Doing this preserves the details, uses less memory,
 
#    and is faster. This may have changed since I tested it.

 
# I commented this out to simplify test script but this is how I suggest to do it.

 
# process(:store) do |io, context|
 
#   io = io.download
 
#   large = resize_to_limit!(io, 1440, 960)
 
#   medium = resize_to_limit!(large, 864, 576)
 
#   small = resize_to_limit!(medium, 461, 307)
 
#   thumb = resize_to_limit!(small, 300, 300)
 
#   { original: io, large: large, medium: medium, small: small, thumb: thumb }
 
# end
end


# Setup database and run migration
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Migration.verbose = false
ActiveRecord::Migration.create_table(:photos) do |t|
  t
.text :image_data
  t
.datetime "created_at",                          null: false
  t
.datetime "updated_at",                          null: false
end


# Model
class Photo < ActiveRecord::Base
  include
PhotoUploader::Attachment.new(:image)
  before_create
:overwrite_created_at  # only overwrite on create, not on every save


 
# 2. The image_data metadata is simplified because I only store one timestamp and not the whole Exif data.
 
def overwrite_created_at
   
self.created_at = DateTime.strptime(self.image.data["metadata"]["original_created_at"], "%Y:%m:%d %H:%M:%S")
 
end




 
# before_save :overwrite_created_at_date     # <======= 4. This will overwrite created_at during every save. You only want to do this one time on create shown above.


 
# def original_created_at
 
#   DateTime.strptime(self.image.data["metadata"]["exif"]["DateTimeOriginal"], "%Y:%m:%d %H:%M:%S")
 
# end


 
# def overwrite_created_at_date
 
#   self.created_at = self.original_created_at
 
# end
end


file
= File.new("../test/fixtures/test_big_image.jpg")
photo
= Photo.create(image: open(file))


puts JSON
(photo.reload.image_data)
# Result =>
# {
#   "id" => "photo/1/image/8b592f8cbde40cab0110ff0b62dabefe.jpg",
#   "storage" => "store",
#   "metadata" => {
#                   "filename"=>"test_big_image.jpg",
#                   "size" => 2290163,
#                   "mime_type" => "image/jpeg",
#                   "original_created_at" => "2008:03:25 20:48:31"
#                   }
# }


puts photo
.inspect
# Result =>
#<Photo id: 1, image_data: "{\"id\":\"photo/1/image/8b592f8cbde40cab0110ff0b62dab...", created_at: "2008-03-25 20:48:31", updated_at: "2018-01-09 02:10:30">

Janko Marohnić

unread,
Jan 9, 2018, 3:26:28 AM1/9/18
to Hiren Mistry, Shrine
When a file is attached to the record, the processing happens only after the record is saved. That means the before the record is saved you will still have a single cached attachment, not a hash of versions. So you need to change

  def original_created_at

    DateTime.strptime(self.image[:large].data["metadata"]["exif"]["DateTimeOriginal"], "%Y:%m:%d %H:%M:%S")

  end


to

  def original_created_at

    DateTime.strptime(self.image.data["metadata"]["exif"]["DateTimeOriginal"], "%Y:%m:%d %H:%M:%S")

  end


You can also replace .data["metadata"] with just .metadata:

  def original_created_at

    DateTime.strptime(self.image.metadata["exif"]["DateTimeOriginal"], "%Y:%m:%d %H:%M:%S")

  end


And then you can replace .metadata["exif"] with .exif, which add_metadata plugin has created for you:

  def original_created_at

    DateTime.strptime(self.image.exif["DateTimeOriginal"], "%Y:%m:%d %H:%M:%S")

  end


As Hiren said, you shouldn't assign the creation date before each save; before creation `image` will be a cached image, but before update it will be a hash of versions, so you would then need to handle that difference. You instead want to do that only when attachment changes:

  def overwrite_created_at_date

    self.created_at = self.original_created_at if image_attacher.changed?

  end


As an extra, if you would like to clean up your model code, you could assign the creation time as a top-level #created_at metadata, and eliminate the #original_created_at method:

  # app/uploaders/image_uploader.rb

  class ImageUploader < Shrine

    add_metadata(:exif) { |io| exif(io) }


    add_metadata :created_at do |io|

      date_time_original = exif(io)["DateTimeOriginal"]

      DateTime.strptime(date_time_original, "%Y:%m:%d %H:%M:%S").iso8601 if date_time_original

    end


    def exif(io)

      MiniMagick::Image.new(io.path).exif

    end

  end


  # app/models/photo.rb

  class Photo < ApplicationRecord

    # ...

    before_save :overwrite_created_at_date


    def overwrite_created_at_date

      self.created_at = Time.iso8601(image.created_at) if image_attacher.changed? && image.created_at

    end

  end


Notice that I also handled the case when `DateTimeOriginal` EXIF doesn't exist (e.g. when the image doesn't have any EXIF data to begin with).

Kind regards,
Janko

--
You received this message because you are subscribed to the Google Groups "Shrine" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ruby-shrine+unsubscribe@googlegroups.com.
To post to this group, send email to ruby-...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/ruby-shrine/107bbe7d-77bd-4500-82d1-e67d37860b22%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages