Class: Image

Inherits:
ApplicationRecord show all
Includes:
Housekeeping, Shared::Attributions, Shared::Citations, Shared::Identifiers, Shared::IsData, Shared::Notes, Shared::ProtocolRelationships, Shared::Tags, SoftValidation
Defined in:
app/models/image.rb

Overview

An Image is just that, as it is stored in the filesystem. No additional metadata beyond file descriptors is included here.

This class relies on the paperclip gem and the ImageMagik app to link, store and manipulate images.

Constant Summary

MISSING_IMAGE_PATH =
'/public/images/missing.jpg'.freeze

Constants included from SoftValidation

SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS

Instance Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Methods included from SoftValidation

#clear_soft_validations, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations

Methods included from Housekeeping

#has_polymorphic_relationship?

Instance Attribute Details

- (Integer) height

Returns the height of the source image in px

Returns:

  • (Integer)

    the height of the source image in px



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

- (String) image_file_content_type

Added by paperclip; the MIME (must be image) and file type (e.g. image/png).

Returns:

  • (String)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

- (String) image_file_file_name

Added by paperclip; the filename after processing to remove special characters.

Returns:

  • (String)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

- (Integer) image_file_file_size

Added by paperclip

Returns:

  • (Integer)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

- (String) image_file_fingerprint

Added by paperclip; MD5 for the image file

Returns:

  • (String)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

- (String) image_file_meta

Added by paperclip_meta gem, stores the sizes of derived images

Returns:

  • (String)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

- (Integer) image_file_updated_at

Added by paperclip

Returns:

  • (Integer)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

- (String) user_file_name

The name of the file as uploaded by the user.

Returns:

  • (String)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

- (Integer) width

Returns the width of the source image in px

Returns:

  • (Integer)

    the width of the source image in px



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/image.rb', line 41

class Image < ApplicationRecord
  include Housekeeping

  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::Attributions
  include Shared::IsData
  include SoftValidation

  MISSING_IMAGE_PATH = '/public/images/missing.jpg'.freeze

  has_many :depictions, inverse_of: :image, dependent: :restrict_with_error

  before_save :extract_tw_attributes

  # also using https://github.com/teeparham/paperclip-meta
  has_attached_file :image_file,
    styles: {medium: ['300x300>', :jpg], thumb: ['100x100>', :png]},
    default_url: MISSING_IMAGE_PATH,
    filename_cleaner:  Utilities::CleanseFilename

  #:restricted_characters => /[^A-Za-z0-9\.]/,
  validates_attachment_content_type :image_file, content_type: /\Aimage\/.*\Z/
  validates_attachment_presence :image_file
  validates_attachment_size :image_file, greater_than: 1.kilobytes

  soft_validate(:sv_duplicate_image?)

  # @return [Boolean]
  def has_duplicate?
    Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
  end

  # @return [Array]
  def duplicate_images
    Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
  end

  # @return [Hash]
  def exif
    # returns a hash of EXIF data if present, empty hash if not.do
    # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

    ret_val = {} # return value

    unless self.new_record? # only process if record exists
      tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
      # following removes the exif, spits and recombines string as a hash
      ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                  .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
      # might be able to tmp.split("\n").collect { |b|
      # b.gsub("exif:", "").split("=")
      # }.inject(ret_val) { |hsh, c|
      #   hsh.merge(c[0] => c[1])
      # }
    end

    ret_val # return
  end

  # @return [Nil]
  def gps_data
    # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
    # (5 digits after decimal point if available)
    # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
    # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

    # check if gps data is in d m s (could be edited manually)
    #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
    # N = +
    # S = -
    # E = +
    # W = -
    # Altitude should be based on reference of sea level
    # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

    # From discussion with Jim -
    # create a utility library called "GeoConvert" and define single method
    # that will convert from degrees min sec to decimal degree
    # - maybe 2 versions? - one returns string, other decimal?

  end

  # @param [ActionController::Parameters] params
  # @return [Scope]
  def self.find_for_autocomplete(params)
    where(id: params[:term]).with_project_id(params[:project_id])
  end

  # Returns the true, unscaled height/width ratio
  # @return [Float]
  def hw_ratio
    raise if height.nil? || width.nil? # if they are something has gone badly wrong
    return (height.to_f / width.to_f)
  end

  # rubocop:disable Style/StringHashKeys
  # used in ImageHelper#image_thumb_tag
  # asthetic scaling of very narrow images in thumbnails
  # @return [Hash]
  def thumb_scaler
    a = self.hw_ratio
    if a < 0.6
      { 'width' => 200, 'height' => 200 * a}
    else
      {}
    end
  end
  # rubocop:enable Style/StringHashKeys

  # the scale factor is typically the same except in a few cases where we skew small thumbs
  # @param [Symbol] size
  # @return [Float]
  def width_scale_for_size(size = :medium)
    (width_for_size(size).to_f / width.to_f)
  end

  # @param [Symbol] size
  # @return [Float]
  def height_scale_for_size(size = :medium)
    height_for_size(size).to_f / height.to_f
  end

  # @param [Symbol] size
  # @return [Float]
  def width_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
    when :medium
      a < 1 ? 640.0 : 640.0 / a
    when :big
      a < 1 ? 1600.0 : 1600.0 / a
    when :original
      width
    else
      nil
    end
  end

  # @param [Symbol] size
  # @return [Float]
  def height_for_size(size = :medium)
    a = self.hw_ratio
    case size
    when :thumb
      a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
    when :medium
      a < 1 ? a * 640 : 640
    when :big
      a < 1 ? a * 1600 : 1600
    when :original
      height
    else
      nil
    end
  end

  #  def filename(layout_section_type)
  #    'tmp/' + tempfile(layout_section_type).path.split('/').last
  #  end

  #  def tempfile(layout_section_type)
  #    tempfile = Tempfile.new([layout_section_type.to_s, '.jpg'], "#{Rails.root.to_s}/public/images/tmp", encoding: 'ASCII-8BIT' )
  #    tempfile.write(zoomed_image(layout_section_type).to_blob)
  #    tempfile
  #  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped(params)
    image = Image.find(params[:id])
    img = Magick::Image.read(image.image_file.path(:original)).first

    cropped = img.crop(
                       params[:x].to_i,
                       params[:y].to_i,
                       params[:width].to_i,
                       params[:height].to_i,
                       true
                      )
    cropped
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized(params)
    c = cropped(params)
    c.resize(params[:new_width].to_i, params[:new_height].to_i)
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box(params)
    c = cropped(params)
    ratio = c.columns.to_f / c.rows.to_f
    box_ratio = params[:box_width].to_f / params[:box_height].to_f

    if box_ratio > 1
      if ratio > 1 # wide into wide
        c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into wide
        c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
      end
    else # <
      if ratio > 1 # wide into tall
        c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
      else # tall into tall
        c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
      end
    end
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.scaled_to_box_blob(params)
    scaled_to_box(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.resized_blob(params)
    resized(params).to_blob
  end

  # @param [ActionController::Parameters] params
  # @return [Magick::Image]
  def self.cropped_blob(params)
    cropped(params).to_blob
  end

  protected

  # @return [Integer, Nil]
  def extract_tw_attributes
    # NOTE: assumes content type is an image.
    tempfile = image_file.queued_for_write[:original]
    if tempfile.nil?
      self.width = 0
      self.height = 0
      self.user_file_name = nil
    else
      self.user_file_name = tempfile.original_filename
      geometry = Paperclip::Geometry.from_file(tempfile)
      self.width = geometry.width.to_i
      self.height = geometry.height.to_i
    end
  end

  # Check md5 fingerprint against existing fingerprints
  # @return [Object]
  def sv_duplicate_image?
    if has_duplicate?
      soft_validations.add(:image_file_fingerprint,
                           'This image is a duplicate of an image already stored.')
    end
  end

end

Class Method Details

+ (Magick::Image) cropped(params)

Parameters:

  • params (ActionController::Parameters)

Returns:

  • (Magick::Image)


215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/models/image.rb', line 215

def self.cropped(params)
  image = Image.find(params[:id])
  img = Magick::Image.read(image.image_file.path(:original)).first

  cropped = img.crop(
                     params[:x].to_i,
                     params[:y].to_i,
                     params[:width].to_i,
                     params[:height].to_i,
                     true
                    )
  cropped
end

+ (Magick::Image) cropped_blob(params)

Parameters:

  • params (ActionController::Parameters)

Returns:

  • (Magick::Image)


272
273
274
# File 'app/models/image.rb', line 272

def self.cropped_blob(params)
  cropped(params).to_blob
end

+ (Scope) find_for_autocomplete(params)

Parameters:

  • params (ActionController::Parameters)

Returns:

  • (Scope)


129
130
131
# File 'app/models/image.rb', line 129

def self.find_for_autocomplete(params)
  where(id: params[:term]).with_project_id(params[:project_id])
end

+ (Magick::Image) resized(params)

Parameters:

  • params (ActionController::Parameters)

Returns:

  • (Magick::Image)


231
232
233
234
# File 'app/models/image.rb', line 231

def self.resized(params)
  c = cropped(params)
  c.resize(params[:new_width].to_i, params[:new_height].to_i)
end

+ (Magick::Image) resized_blob(params)

Parameters:

  • params (ActionController::Parameters)

Returns:

  • (Magick::Image)


266
267
268
# File 'app/models/image.rb', line 266

def self.resized_blob(params)
  resized(params).to_blob
end

+ (Magick::Image) scaled_to_box(params)

Parameters:

  • params (ActionController::Parameters)

Returns:

  • (Magick::Image)


238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'app/models/image.rb', line 238

def self.scaled_to_box(params)
  c = cropped(params)
  ratio = c.columns.to_f / c.rows.to_f
  box_ratio = params[:box_width].to_f / params[:box_height].to_f

  if box_ratio > 1
    if ratio > 1 # wide into wide
      c.resize(params[:box_width ].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
    else # tall into wide
      c.resize((params[:box_width ].to_f * ratio / box_ratio).to_i, params[:box_height].to_i )
    end
  else # <
    if ratio > 1 # wide into tall
      c.resize(params[:box_width].to_i, (params[:box_height].to_f / ratio * box_ratio).to_i)
    else # tall into tall
      c.resize((params[:box_width ].to_f * ratio * box_ratio ).to_i, (params[:box_height].to_f ).to_i)
    end
  end
end

+ (Magick::Image) scaled_to_box_blob(params)

Parameters:

  • params (ActionController::Parameters)

Returns:

  • (Magick::Image)


260
261
262
# File 'app/models/image.rb', line 260

def self.scaled_to_box_blob(params)
  scaled_to_box(params).to_blob
end

Instance Method Details

- (Array) duplicate_images

Returns:

  • (Array)


78
79
80
# File 'app/models/image.rb', line 78

def duplicate_images
  Image.where(image_file_fingerprint: self.image_file_fingerprint).not_self(self).to_a
end

- (Hash) exif

Returns:

  • (Hash)


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/models/image.rb', line 83

def exif
  # returns a hash of EXIF data if present, empty hash if not.do
  # EXIF data tags/specifications -  http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF

  ret_val = {} # return value

  unless self.new_record? # only process if record exists
    tmp     = `identify -format "%[EXIF:*]" #{self.image_file.url}` # returns a string (exif:tag=value\n)
    # following removes the exif, spits and recombines string as a hash
    ret_val = tmp.split("\n").collect { |b| b.gsub('exif:', '').split('=') }
                .inject({}) { |hsh, c| hsh.merge(c[0] => c[1]) }
    # might be able to tmp.split("\n").collect { |b|
    # b.gsub("exif:", "").split("=")
    # }.inject(ret_val) { |hsh, c|
    #   hsh.merge(c[0] => c[1])
    # }
  end

  ret_val # return
end

- (Integer, Nil) extract_tw_attributes (protected)

Returns:

  • (Integer, Nil)


279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/models/image.rb', line 279

def extract_tw_attributes
  # NOTE: assumes content type is an image.
  tempfile = image_file.queued_for_write[:original]
  if tempfile.nil?
    self.width = 0
    self.height = 0
    self.user_file_name = nil
  else
    self.user_file_name = tempfile.original_filename
    geometry = Paperclip::Geometry.from_file(tempfile)
    self.width = geometry.width.to_i
    self.height = geometry.height.to_i
  end
end

- (Nil) gps_data

Returns:

  • (Nil)


105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'app/models/image.rb', line 105

def gps_data
  # if there is EXIF data, pulls out geographic coordinates & returns hash of lat/long in decimal degrees
  # (5 digits after decimal point if available)
  # EXIF gps information is in http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF section 4.6.6
  # note that cameras follow specifications, but EXIF data can be edited manually and may not follow specifications.

  # check if gps data is in d m s (could be edited manually)
  #   => format dd/1,mm/1,ss/1 or dd/1,mmmm/100,0/1 or 40/1, 5/1, 314437/10000
  # N = +
  # S = -
  # E = +
  # W = -
  # Altitude should be based on reference of sea level
  # GPSAltitudeRef is 0 for above sea level, and 1 for below sea level

  # From discussion with Jim -
  # create a utility library called "GeoConvert" and define single method
  # that will convert from degrees min sec to decimal degree
  # - maybe 2 versions? - one returns string, other decimal?

end

- (Boolean) has_duplicate?

Returns:

  • (Boolean)


73
74
75
# File 'app/models/image.rb', line 73

def has_duplicate?
  Image.where(image_file_fingerprint: self.image_file_fingerprint).count > 1
end

- (Float) height_for_size(size = :medium)

Parameters:

  • size (Symbol) (defaults to: :medium)

Returns:

  • (Float)


187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'app/models/image.rb', line 187

def height_for_size(size = :medium)
  a = self.hw_ratio
  case size
  when :thumb
    a < 0.6 ? 213.0 * height.to_f / width.to_f : 160
  when :medium
    a < 1 ? a * 640 : 640
  when :big
    a < 1 ? a * 1600 : 1600
  when :original
    height
  else
    nil
  end
end

- (Float) height_scale_for_size(size = :medium)

Parameters:

  • size (Symbol) (defaults to: :medium)

Returns:

  • (Float)


163
164
165
# File 'app/models/image.rb', line 163

def height_scale_for_size(size = :medium)
  height_for_size(size).to_f / height.to_f
end

- (Float) hw_ratio

Returns the true, unscaled height/width ratio

Returns:

  • (Float)


135
136
137
138
# File 'app/models/image.rb', line 135

def hw_ratio
  raise if height.nil? || width.nil? # if they are something has gone badly wrong
  return (height.to_f / width.to_f)
end

- (Object) sv_duplicate_image? (protected)

Check md5 fingerprint against existing fingerprints

Returns:

  • (Object)


296
297
298
299
300
301
# File 'app/models/image.rb', line 296

def sv_duplicate_image?
  if has_duplicate?
    soft_validations.add(:image_file_fingerprint,
                         'This image is a duplicate of an image already stored.')
  end
end

- (Hash) thumb_scaler

rubocop:disable Style/StringHashKeys used in ImageHelper#image_thumb_tag asthetic scaling of very narrow images in thumbnails

Returns:

  • (Hash)


144
145
146
147
148
149
150
151
# File 'app/models/image.rb', line 144

def thumb_scaler
  a = self.hw_ratio
  if a < 0.6
    { 'width' => 200, 'height' => 200 * a}
  else
    {}
  end
end

- (Float) width_for_size(size = :medium)

Parameters:

  • size (Symbol) (defaults to: :medium)

Returns:

  • (Float)


169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'app/models/image.rb', line 169

def width_for_size(size = :medium)
  a = self.hw_ratio
  case size
  when :thumb
    a < 0.6 ? 200.0 : ((width.to_f / height.to_f ) * 160.0)
  when :medium
    a < 1 ? 640.0 : 640.0 / a
  when :big
    a < 1 ? 1600.0 : 1600.0 / a
  when :original
    width
  else
    nil
  end
end

- (Float) width_scale_for_size(size = :medium)

the scale factor is typically the same except in a few cases where we skew small thumbs

Parameters:

  • size (Symbol) (defaults to: :medium)

Returns:

  • (Float)


157
158
159
# File 'app/models/image.rb', line 157

def width_scale_for_size(size = :medium)
  (width_for_size(size).to_f / width.to_f)
end