Module: Models::Imageable
- Extended by:
- ActiveSupport::Concern
- Included in:
- Api::Image, Image
- Defined in:
- app/concerns/models/imageable.rb
Constant Summary collapse
- STANDARD_THUMBNAIL_SIZE =
'400x400>'- STANDARD_SIZES =
{ thumb: '30x30>', small: '100x100>', medium: '300x300>', large: '400x400>', extra_large: '600x600>' }
- 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]
Class Method Summary collapse
Instance Method Summary collapse
-
#aspect_ratio ⇒ Object
Width / Height.
- #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
248 249 250 |
# File 'app/concerns/models/imageable.rb', line 248 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
64 65 66 67 68 |
# File 'app/concerns/models/imageable.rb', line 64 def aspect_ratio return unless && .to_f / end |
#human_size ⇒ Object
53 54 55 56 57 |
# File 'app/concerns/models/imageable.rb', line 53 def human_size return unless .present? 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)
263 264 265 |
# File 'app/concerns/models/imageable.rb', line 263 def ik_file_name slug end |
#ik_file_name_with_extension ⇒ Object
Returns the full filename with extension (for display/download purposes)
268 269 270 |
# File 'app/concerns/models/imageable.rb', line 268 def ik_file_name_with_extension .present? ? "#{slug}.#{}" : slug end |
#ik_get_file_details ⇒ Object
298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'app/concerns/models/imageable.rb', line 298 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
286 287 288 289 290 291 292 293 294 295 296 |
# File 'app/concerns/models/imageable.rb', line 286 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
312 313 314 315 316 |
# File 'app/concerns/models/imageable.rb', line 312 def # Use ImageKitFactory helper method res = ImageKitFactory.(ik_raw_url) res&.to_h&.symbolize_keys end |
#ik_raw_url ⇒ Object
272 273 274 |
# File 'app/concerns/models/imageable.rb', line 272 def ik_raw_url "https://#{IK_HOSTNAME}#{ik_path}" end |
#ik_url(transformations: [], query_parameters: {}, alternate_path: nil, transformation_position: nil) ⇒ Object
276 277 278 279 280 281 282 283 284 |
# File 'app/concerns/models/imageable.rb', line 276 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
70 71 72 73 |
# File 'app/concerns/models/imageable.rb', line 70 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
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 |
# File 'app/concerns/models/imageable.rb', line 102 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.map(&:presence).compact ik_url(transformations: transformations, query_parameters: query_parameters, transformation_position: transformation_position) end |
#info ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'app/concerns/models/imageable.rb', line 24 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
75 76 77 78 79 80 81 82 83 84 85 |
# File 'app/concerns/models/imageable.rb', line 75 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
253 254 255 256 257 258 |
# File 'app/concerns/models/imageable.rb', line 253 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
15 16 17 18 19 20 21 22 |
# File 'app/concerns/models/imageable.rb', line 15 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
59 60 61 |
# File 'app/concerns/models/imageable.rb', line 59 def to_s [title, , 'Unknown'].detect(&:present?) + " [#{id}]" end |