Module: Models::Imageable
- Extended by:
- ActiveSupport::Concern
- Included in:
- Api::Image, Image
- Defined in:
- app/concerns/models/imageable.rb
Overview
ActiveSupport::Concern mixin: imageable.
Constant Summary collapse
- STANDARD_THUMBNAIL_SIZE =
Standard thumbnail size.
'400x400>'.freeze
- STANDARD_SIZES =
Standard sizes.
{ thumb: '30x30>', small: '100x100>', medium: '300x300>', large: '400x400>', extra_large: '600x600>' }.freeze
- VALID_IMAGE_URL_OPTIONS =
Available valid image url options.
%i[encode_format size borderx bordery size_modifier percentage width height crop_x crop_y crop_w crop_h crop_mode crop_last rotate relative focus hostname cache webp download optimize thumbnail background progressive_jpeg blur].freeze
Class Method Summary collapse
Instance Method Summary collapse
-
#aspect_ratio ⇒ Object
Width / Height.
-
#aspect_ratio_label ⇒ Object
Human-readable aspect ratio (e.g. "16:9", "4:5", "1.91:1").
- #human_size ⇒ Object
-
#ik_file_name ⇒ Object
Returns the filename for ImageKit operations (without extension) ImageKit files are stored without extensions to allow dynamic format negotiation (ImageKit can serve optimal formats like WebP/AVIF based on browser support).
-
#ik_file_name_with_extension ⇒ Object
Returns the full filename with extension (for display/download purposes).
- #ik_get_file_details ⇒ Object
- #ik_get_metadata ⇒ Object
- #ik_get_metadata_by_url ⇒ Object
- #ik_raw_url ⇒ Object
- #ik_url(transformations: [], query_parameters: {}, alternate_path: nil, transformation_position: nil) ⇒ Object
- #image_info ⇒ Object
-
#image_url(args = nil) ⇒ Object
encode_format: nil, webp: nil, size: nil, size_modifier: nil, percentage: nil, width: nil, height: nil, borderx: nil, bordery: nil, crop_x: nil, crop_y: nil, crop_w: nil, crop_h: nil, crop_mode: nil, rotate: nil, relative: nil, hostname: nil, protocol: nil, cache: nil, download: false, optimize: nil, thumbnail: false, background: nil dpr: auto For a full list of possible imagekit transformations see https://github.com/imagekit-developer/imagekit-ruby.
- #info ⇒ Object
- #presets_hash ⇒ Object
-
#sourceset(encode_format: nil) ⇒ Object
New simplified form using image url.
- #thumbnail_url(options = {}) ⇒ Object
- #to_s ⇒ Object
Class Method Details
.suggested_sources_for_select ⇒ Object
270 271 272 |
# File 'app/concerns/models/imageable.rb', line 270 def self.suggested_sources_for_select Image.where.not(source: [nil, '']).order(:source).distinct.pluck(:source) end |
Instance Method Details
#aspect_ratio ⇒ Object
Width / Height
68 69 70 71 72 |
# File 'app/concerns/models/imageable.rb', line 68 def aspect_ratio return unless && .to_f / end |
#aspect_ratio_label ⇒ Object
Human-readable aspect ratio (e.g. "16:9", "4:5", "1.91:1"). Uses GCD to
reduce to integer form when both sides fit under 100; otherwise falls back
to a ":1" approximation for odd ratios like 699:157.
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'app/concerns/models/imageable.rb', line 77 def aspect_ratio_label return unless && w, h = , g = w.gcd(h) w_r = w / g h_r = h / g if w_r < 100 && h_r < 100 "#{w_r}:#{h_r}" else format('%.2f:1', w.to_f / h) end end |
#human_size ⇒ Object
57 58 59 60 61 |
# File 'app/concerns/models/imageable.rb', line 57 def human_size return if .blank? ActionController::Base.helpers.number_to_human_size() end |
#ik_file_name ⇒ Object
Returns the filename for ImageKit operations (without extension)
ImageKit files are stored without extensions to allow dynamic format negotiation
(ImageKit can serve optimal formats like WebP/AVIF based on browser support)
285 286 287 |
# File 'app/concerns/models/imageable.rb', line 285 def ik_file_name slug end |
#ik_file_name_with_extension ⇒ Object
Returns the full filename with extension (for display/download purposes)
290 291 292 |
# File 'app/concerns/models/imageable.rb', line 290 def ik_file_name_with_extension .present? ? "#{slug}.#{}" : slug end |
#ik_get_file_details ⇒ Object
320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'app/concerns/models/imageable.rb', line 320 def ik_get_file_details # file id? use that # ImageKit 4.0 returns snake_case keys: file_id instead of fileId if (file_id = asset&.dig('file_id') || asset&.dig('fileId')) # Use ImageKitFactory helper method res = ImageKitFactory.get_file(file_id) res&.to_h&.deep_symbolize_keys&.with_indifferent_access else # Try a search by Image-<id> # Use ImageKitFactory helper method res = ImageKitFactory.list_assets(tags: ["image-#{id}", "Image-#{id}"]) res&.items&.first&.to_h&.deep_symbolize_keys&.with_indifferent_access end end |
#ik_get_metadata ⇒ Object
308 309 310 311 312 313 314 315 316 317 318 |
# File 'app/concerns/models/imageable.rb', line 308 def # file id? use that # ImageKit 4.0 returns snake_case keys: file_id instead of fileId if (file_id = asset&.dig('file_id') || asset&.dig('fileId')) # Use ImageKitFactory helper method res = ImageKitFactory.(file_id) res&.to_h&.symbolize_keys else # try url end end |
#ik_get_metadata_by_url ⇒ Object
334 335 336 337 338 |
# File 'app/concerns/models/imageable.rb', line 334 def # Use ImageKitFactory helper method res = ImageKitFactory.(ik_raw_url) res&.to_h&.symbolize_keys end |
#ik_raw_url ⇒ Object
294 295 296 |
# File 'app/concerns/models/imageable.rb', line 294 def ik_raw_url "https://#{IK_HOSTNAME}#{ik_path}" end |
#ik_url(transformations: [], query_parameters: {}, alternate_path: nil, transformation_position: nil) ⇒ Object
298 299 300 301 302 303 304 305 306 |
# File 'app/concerns/models/imageable.rb', line 298 def ik_url(transformations: [], query_parameters: {}, alternate_path: nil, transformation_position: nil) # Use ImageKitFactory helper method to build URL ImageKitFactory.build_url( src: alternate_path || ik_path, transformations: transformations, query_parameters: query_parameters, transformation_position: transformation_position || 'query' ) end |
#image_info ⇒ Object
91 92 93 94 |
# File 'app/concerns/models/imageable.rb', line 91 def image_info # attachment.identify('-verbose').gsub(" ", " ") rescue nil (asset || )&.to_yaml end |
#image_url(args = nil) ⇒ Object
encode_format: nil, webp: nil,
size: nil, size_modifier: nil,
percentage: nil,
width: nil, height: nil, borderx: nil, bordery: nil,
crop_x: nil, crop_y: nil, crop_w: nil, crop_h: nil, crop_mode: nil, rotate: nil,
relative: nil, hostname: nil, protocol: nil, cache: nil,
download: false, optimize: nil, thumbnail: false, background: nil
dpr: auto
For a full list of possible imagekit transformations see https://github.com/imagekit-developer/imagekit-ruby
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 |
# File 'app/concerns/models/imageable.rb', line 124 def image_url(args = nil) args = args&.dup # Copy to ensure we can modify without modifying the original args ||= {} args = args.symbolize_keys query_parameters = {} transformations = [] transformation_position = args[:transformation_position] # path or query, query is default # Cropping applies first # crop_x: nil, crop_y: nil, crop_w: nil, crop_h: nil crop_transform = {} crop_transform[:x] = args[:crop_x].presence crop_transform[:y] = args[:crop_y].presence crop_transform[:width] = args[:crop_w].presence crop_transform[:height] = args[:crop_h].presence crop_transform = crop_transform.compact # Only apply crop mode if we have a crop request, delete the crop mode so it doesn't get applied down the stream crop_transform[:crop_mode] = args.delete(:crop_mode).presence || 'force' if crop_transform.present? transformations << crop_transform unless args[:crop_last].to_b transformations << { named: args[:named] } if args[:named] # https://docs.imagekit.io/features/image-transformations/resize-crop-and-other-transformations # size does not exist so we have to extract width and height # sizing is done as a group, size will be taken as authoritative over width and height resize_operation = {} if args[:size].present? w, h = args[:size].split('x').map(&:presence) resize_operation[:width] = w.to_i if w resize_operation[:height] = h.to_i if h elsif args[:width].present? || args[:height].present? resize_operation[:width] = args[:width].to_i if args[:width].present? resize_operation[:height] = args[:height].to_i if args[:height].present? elsif args[:percentage] && args[:percentage].to_i > 0 && args[:percentage].to_i < 100 # In ik, this translates to a value between 0 and 1 for width and height pf = (args[:percentage].to_f / 100).round(2) resize_operation[:width] = pf resize_operation[:height] = pf end # if a width and height are specified, we need a padding technique if the image is not the 1:1 aspect ratio # we will assume a white background if resize_operation[:width] && resize_operation[:height] resize_operation[:cm] = args[:crop_mode].presence || 'pad_resize' unless args[:crop_mode] == :none # Strip leading # from background colors (e.g., '#ffffff' -> 'ffffff') # The # character breaks URLs as it starts a fragment identifier resize_operation[:bg] = (args[:background] || 'FFFFFF').to_s.delete_prefix('#') end transformations << resize_operation if resize_operation.present? # https://imagekit.io/blog/smart-crop-deliver-perfect-responsive-images/ # fo (focus) MUST be in the same transformation step as w/h/cm for smart-crop to work. # We mutate resize_operation in-place — it's already referenced in transformations. if args[:focus].present? if resize_operation[:width] && resize_operation[:height] resize_operation[:focus] = args[:focus] else transformations << { focus: args[:focus] } end elsif args[:thumbnail] if resize_operation[:width] && resize_operation[:height] resize_operation[:focus] = 'auto' else transformations << { focus: 'auto' } end end if (zoom = args[:zoom].presence) transformations << { z: zoom } end if (radius = args[:radius].presence) transformations << { r: radius } end if (b = args[:border].presence || args[:borderx].presence || args[:bordery].presence) # there's no concept of x and y so we'll just take the first one # border color is possible but we didn't use this so we'll default to, gray? # If you pass true we'll use a nominal value of 3 px b = 3 if b == true # Strip leading # from colors (e.g., '#555555' -> '555555') bc = (args[:border_color] || args[:background] || '555555').to_s.delete_prefix('#') bs = "#{b.to_i}_#{bc}" transformations << { b: bs } end if (rt = args[:rotate].presence) transformations << { rt: rt } end # Blur transformation (1-100, higher = more blur) # Useful for LQIP (Low Quality Image Placeholder) poster images if (blur_value = args[:blur].presence) transformations << { bl: blur_value.to_i.clamp(1, 100) } end transformations << crop_transform if args[:crop_last].to_b if args[:dpr] != :ignore if args[:dpr].present? transformations << { dpr: args[:dpr] } elsif args[:dpr].nil? # Because if we do false, it will be removed, but by default having auto is a good idea transformations << { dpr: 'auto' } end end if (format = args[:encode_format].presence&.to_s) && format != 'original' # Normalize format = 'jpeg' if format == 'jpg' t = {} # We only encode the format if the requested format is different # Or if webp (which is auto optimization or can be applied to avif) is false # Or if download is true if !args[:webp].to_b || args[:download].to_b || format != t[:f] = format # if our requested format is jpeg, go progressive t[:pr] = (args[:progressive_jpeg].nil? || args[:progressive_jpeg] == true) if format == 'jpeg' end # A quality param supplied can be specified # to override the 85 default t[:q] = args[:quality].to_i if args[:quality].present? # JPEG doesn't support transparency, so always provide a background color # when converting to JPEG. This prevents black backgrounds on transparent PNGs, # even when attachment_format is not set. if format == 'jpeg' # Strip leading # from background colors (e.g., '#ffffff' -> 'ffffff') bc = (args[:background] || 'FFFFFF').to_s.delete_prefix('#') t[:bg] = bc end transformations << t end if args[:download].present? # Force download in native format # transformations.delete_if { |v| v[:f].present? } # transformations << { f: attachment_format } query_parameters['ik-attachment'] = true end # Remove empty transformations transformations = transformations.filter_map(&:presence) ik_url(transformations: transformations, query_parameters: query_parameters, transformation_position: transformation_position) end |
#info ⇒ Object
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'app/concerns/models/imageable.rb', line 28 def info attrs = [] attrs << "Title: #{title}" attrs << "Ref: #{reference_number}" begin attrs << "File Name: #{}" rescue StandardError nil end begin attrs << "Format: #{}" rescue StandardError nil end begin attrs << "Dimensions (WxH): #{dimensions}" rescue StandardError nil end begin attrs << "Size: #{human_size}" rescue StandardError nil end attrs << "Colorspace: #{image_colorspace}" attrs << "DPI: #{image_dpi}" attrs.compact.join("\n") end |
#presets_hash ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 |
# File 'app/concerns/models/imageable.rb', line 96 def presets_hash { 'Original' => dimensions, 'Thumb Image' => '30x30>', 'Small Image' => '100x100>', 'Medium Image' => '300x300>', 'Large Image' => '400x400>', 'Extra Large Image' => '600x600>', 'XXL Image' => '900x900>' } end |
#sourceset(encode_format: nil) ⇒ Object
New simplified form using image url
275 276 277 278 279 280 |
# File 'app/concerns/models/imageable.rb', line 275 def sourceset(encode_format: nil) srcsets = [75, 50, 25].map do |p| "#{image_url(percentage: p, encode_format: encode_format)} #{(( * p) / 100).round}w" end srcsets.join(',') end |
#thumbnail_url(options = {}) ⇒ Object
19 20 21 22 23 24 25 26 |
# File 'app/concerns/models/imageable.rb', line 19 def thumbnail_url( = {}) return unless asset = .dup dimensions = .delete(:dimensions) || STANDARD_THUMBNAIL_SIZE image_url(size: dimensions.to_s, thumbnail: true, **) end |
#to_s ⇒ Object
63 64 65 |
# File 'app/concerns/models/imageable.rb', line 63 def to_s [title, , 'Unknown'].find(&:present?) + " [#{id}]" end |