Class: Image::LegacyImageParamTranslator

Inherits:
Object
  • Object
show all
Defined in:
app/services/image/legacy_image_param_translator.rb

Overview

This class translates params from the legacy format e.g /img/w_400,h_200/
to the imagekit format /img/?tr=w-400,h-200
LEGACY: THIS CODE IS FOR REFERENCE ONLY, DO NOT USE WITHOUT REFACTORING AND TESTING

Constant Summary collapse

REGEXP =

e.g. /img/s_600x600%3E/cross-section-of-a-tile-floor-subfloor-electric-heating-roll-thinset-and-tile-flooring-79ecae.jpg
Updated to exclude ? from capture groups to handle URLs with query strings (e.g., ?utm_source=chatgpt.com)

%r{\A/img/?([^/?]*)?/([^/?]*)\.([a-zA-Z]{3,4})(?:\?.*)?\z}
REGEXP_FULL_URL =
%r{https://\w{3}\.warmlyyours\.com/img/?([^/?]*)?/([^/?]*)\.([a-zA-Z]{3,4})}
REGEXP_FULL_URL_WITH_LOCALE =
%r{https://\w{3}\.warmlyyours\.com/\+\+locale\+\+/img/?([^/?]*)?/([^/?]*)\.([a-zA-Z]{3,4})}

Class Method Summary collapse

Class Method Details

.to_transformations(processing_string, encode_format = nil, stored_file_format = nil) ⇒ Object

Returns translated transformations



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
# File 'app/services/image/legacy_image_param_translator.rb', line 92

def self.to_transformations(processing_string, encode_format = nil, stored_file_format = nil)
  processing_string = Addressable::URI.unescape(processing_string.to_s).presence
  process_tokens = processing_string&.split(',') || []
  transformations = []
  width = nil
  height = nil
  crop_x = nil
  crop_y = nil
  crop_w = nil
  crop_h = nil
  rotate = nil
  background = nil
  border_x = nil
  border_y = nil
  blur = nil
  encode_format = encode_format&.downcase
  encode_format = 'jpeg' if encode_format == 'jpg'
  encode_format = nil unless %w[jpeg gif svg png webp jp2 avif].include?(encode_format)

  process_tokens.each do |instruction| # e.g. w_100
    cmd_method, value = instruction.split('_')
    next if value.nil? # Skip malformed instructions without a value

    # Process sizing first
    case cmd_method
    when 's' # Size
      size = CGI.unescapeHTML(value)
      # Do we have a x ?
      unless size.index('x')
        # Add as a width
        size = "#{size}x"
      end
      width, height, modifier = size.scan(/(\d+)x(\d+)?(.?)/)[0]
    when 'w' # Width
      width = value.scan(/(\d+)/)[0][0].to_i
    when 'h' # Height
      height = value.scan(/(\d+)/)[0][0].to_i
    when 'p' # Percentage of original size
      value = value.scan(/(\d+)/)[0][0].to_f
      pf = (value / 100).round(2)
      width = pf
      height = pf
    when 'cx' # Crop start from X
      crop_x = value.scan(/(\d+)/)[0][0].to_i
    when 'cy' # Crop start from y
      crop_y = value.scan(/(\d+)/)[0][0].to_i
    when 'cw' # Crop width
      crop_w = value.scan(/(\d+)/)[0][0].to_i
    when 'ch' # Crop height
      crop_h = value.scan(/(\d+)/)[0][0].to_i
    when 'r' # Rotate the image in degrees
      rotate = value.scan(/(\d+)/)[0][0].to_i
    when 'bg'
      background = value
    when 'bx'
      border_x = value.scan(/(\d+)/)[0][0].to_i
    when 'by'
      border_y = value.scan(/(\d+)/)[0][0].to_i
    when 'bl'
      blur = value.to_b
    end
    # when 'c' # Don't cache the output, set cache headers to expire right away
    #  cache_output = value.to_b
    # when 'o' # Don't optimize, including don't webp the file
    #  optimize = value.to_b
    # when 'wp' # Don't webp the file for chrome (but still optimize)
    #  webp = value.to_b
    # when 'd' # Send the file as content disposition attachment
    #  download = value.to_b
    # when 't' # Thumbnail mode, will crop according to size with center of gravity
    #  thumbnail = value.to_b
  end # End looping through commands

  # Crop/extract FIRST (operates on original image dimensions)
  transformations << { crop_mode: 'extract', x: crop_x, y: crop_y, width: crop_w, height: crop_h } if crop_x && crop_y && crop_w && crop_h

  # Then resize (operates on the cropped result, or original if no crop)
  if width && height
    transformations << { width: width, height: height, cm: 'pad_resize', bg: background || 'FFFFFF' }
  elsif width || height
    transformations << { width: width, height: height }.compact
  end
  # rotate
  transformations << { rotation: rotate } if rotate

  if border_x || border_y
    b = border_x || border_y
    bc = background || '555555'
    bs = "#{b.to_i}_#{bc}"
    transformations << { border: b, background: background }
  end

  transformations << { background: background } if background

  # Redundant to encode format if the stored format is the same
  transformations << { format: encode_format } if encode_format && (stored_file_format.nil? || stored_file_format != encode_format)

  transformations
end

.translate_from_args(**args) ⇒ Object



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
# File 'app/services/image/legacy_image_param_translator.rb', line 192

def self.translate_from_args(**args)
  args ||= {}
  transformations = []
  # 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
  if args[:size].present?
    w, h = args[:size].split('x').map(&:presence)
    transformations << { width: w&.to_i, height: h&.to_i }.compact
  elsif args[:width] || args[:height]
    transformations << { width: args[:width]&.to_i, height: args[:height]&.to_i }.compact
  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)
    transformations << { width: pf, height: pf }
  end

  # Cropping will apply after resizing
  # crop_x: nil, crop_y: nil, crop_w: nil, crop_h: nil
  if crop_x = args[:crop_x] && crop_y = args[:crop_y] && crop_w = args[:crop_w] && crop_h = args[:crop_h]
    transformations << { cm: 'extract', x: crop_x, y: crop_y, width: crop_w, height: crop_h }
  end

  if b = args[:border] || args[:borderx] || args[:bordery]
    # 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
    bc = args[:border_color] || args[:background] || '555555'
    bs = "#{b.to_i}_#{bc}"
    transformations << { b: bs }
  end

  if rt = args[:rotate]
    transformations << { rt: rt }
  end

  if format = args[:encode_format]
    transformations << { f: format }
  end
  transformations
end

.translate_from_components(processing_string, file_id, requested_file_format, rack_env = nil) ⇒ Object

processing_string: a legacy processing string, e.g w_100,h_100 or s_300x300!
file_id: the slug stored in our database
requested_file_format: the file format requested. Note that our image kit url will always refer to what is
stored
rack_env: the rack environment at time of request for logging additional info



39
40
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
# File 'app/services/image/legacy_image_param_translator.rb', line 39

def self.translate_from_components(processing_string, file_id, requested_file_format, rack_env = nil)
  begin
    img = Image.friendly.find(file_id)
    stored_file_format = img.attachment_format
  rescue ActiveRecord::RecordNotFound
    extra_data = rack_env&.select { |key, value| key.include?('HTTP_') || key.include?('REQUEST_') } || {}
    extra_data['FILE_ID'] = file_id
    ErrorReporting.warning("Could not find image #{file_id}", extra_data)
    Rails.logger.warn('Could not find image', file_id: file_id)
  end

  requested_file_format = requested_file_format&.downcase

  transformations = to_transformations(processing_string, requested_file_format, stored_file_format)

  # Legacy crop coordinate safety check:
  # In the old image processing system, crop coordinates (cx, cy, cw, ch) were often
  # set via CropperJS against a scaled-down preview of the image, NOT the full-resolution
  # original. ImageKit's cm-extract applies coordinates to the full-resolution original,
  # which produces wrong results when the coordinates were set on a smaller version.
  #
  # Detection: if the crop area is <5% of the original image area, the coordinates
  # were almost certainly from a scaled-down preview and cannot be reliably applied
  # to the original. In that case, we drop the crop step and fall back to resize-only,
  # which shows the full image at the requested dimensions.
  if img && (orig_w = img.ik_width || img.try(:attachment_width)) && (orig_h = img.ik_height || img.try(:attachment_height))
    transformations.reject! do |t|
      if t[:crop_mode] == 'extract' && t[:width] && t[:height] && orig_w > 0 && orig_h > 0
        crop_area_ratio = (t[:width].to_f * t[:height]) / (orig_w * orig_h)
        if crop_area_ratio < 0.05
          Rails.logger.info(
            "Legacy crop coordinates dropped (#{crop_area_ratio.round(4)} of original area)",
            file_id: file_id, crop: t.slice(:x, :y, :width, :height),
            original: { width: orig_w, height: orig_h }
          )
          true
        end
      end
    end
  end

  # Use the actual ImageKit path from the asset record when available.
  # This is critical because most images are stored WITH file extensions in ImageKit
  # (e.g., /img/my-image-abc123.jpeg), but ik_file_name returns only the slug
  # (e.g., my-image-abc123). Using the slug alone produces a 404 on ImageKit.
  # Fall back to constructing a path from file_id + requested format for unknown images.
  ik_path = img&.ik_path || "/img/#{file_id}.#{requested_file_format}"

  # Use ImageKitFactory helper method to build URL
  ImageKitFactory.build_url(src: ik_path, transformations: transformations)
end

.translate_from_path(path, rack_env = nil) ⇒ Object

Provide a path, e.g. /img/w_100,h_100/image123.png and get a ik url back
Optionally you can specify force_jpeg: true to enforce jpeg when png is requested



29
30
31
32
# File 'app/services/image/legacy_image_param_translator.rb', line 29

def self.translate_from_path(path, rack_env = nil)
  processing_string, file_id, file_format = path.scan(REGEXP)&.first
  translate_from_components(processing_string, file_id, file_format, rack_env)
end

.translate_from_url(url, rack_env = nil) ⇒ Object

Provide a URL, e.g. https://ik.warmlyyours.com/img/image123.png?tr=w-100,h-100,cm-pad_resize,bg-FFFFFF:f-png and get a ik url back
Optionally you can specify force_jpeg: true to enforce jpeg when png is requested



16
17
18
19
# File 'app/services/image/legacy_image_param_translator.rb', line 16

def self.translate_from_url(url, rack_env = nil)
  processing_string, file_id, file_format = url.scan(REGEXP_FULL_URL)&.first
  translate_from_components(processing_string, file_id, file_format, rack_env)
end

.translate_from_url_with_locale(url, rack_env = nil) ⇒ Object

Same as above but for urls that include locale }



22
23
24
25
# File 'app/services/image/legacy_image_param_translator.rb', line 22

def self.translate_from_url_with_locale(url, rack_env = nil)
  processing_string, file_id, file_format = url.scan(REGEXP_FULL_URL_WITH_LOCALE)&.first
  translate_from_components(processing_string, file_id, file_format, rack_env)
end