@@ -237,6 +237,102 @@ def get_authentication_parameters(token: nil, expire: nil)
237237 get_authentication_parameters_internal ( final_token , final_expire , @client . private_key )
238238 end
239239
240+ # Generates responsive image attributes for use in HTML <img> tags.
241+ #
242+ # This method creates optimized srcset and sizes attributes for responsive images,
243+ # enabling browsers to select the most appropriate image size based on the device's
244+ # screen width and resolution. Supports three strategies:
245+ # - Width-based (w descriptors): When sizes attribute is provided
246+ # - DPR-based (x descriptors): When width is provided without sizes
247+ # - Fallback (w descriptors): Uses device breakpoints when neither is provided
248+ #
249+ # @param options [Hash] Options for generating responsive image attributes
250+ # @option options [String] :src Required. The relative or absolute path of the image
251+ # @option options [String] :url_endpoint Required. Your ImageKit URL endpoint
252+ # @option options [Integer] :width The intended display width in pixels, used only when sizes is not provided.
253+ # Triggers a DPR-based strategy (1x and 2x variants) and generates x descriptors in srcSet. Ignored if sizes is present.
254+ # @option options [String] :sizes The value for the HTML sizes attribute (e.g., "100vw" or "(min-width:768px) 50vw, 100vw").
255+ # If it includes one or more vw units, breakpoints smaller than the corresponding percentage of the smallest device width are excluded.
256+ # If it contains no vw units, the full breakpoint list is used. Enables a width-based strategy and generates w descriptors in srcSet.
257+ # @option options [Array<Integer>] :device_breakpoints Custom list of device-width breakpoints in pixels.
258+ # These define common screen widths for responsive image generation. Defaults to [640, 750, 828, 1080, 1200, 1920, 2048, 3840]. Sorted automatically.
259+ # @option options [Array<Integer>] :image_breakpoints Custom list of image-specific breakpoints in pixels.
260+ # Useful for generating small variants (e.g., placeholders or thumbnails). Merged with device_breakpoints before calculating srcSet.
261+ # Defaults to [16, 32, 48, 64, 96, 128, 256, 384]. Sorted automatically.
262+ # @option options [Array<Hash>] :transformation Array of transformation objects to apply
263+ # @option options [Symbol] :transformation_position Where to add transformations (:path or :query)
264+ # @option options [Hash] :query_parameters Additional query parameters to add to URLs
265+ # @return [Hash] Hash containing responsive image attributes suitable for an HTML <img> element:
266+ # - :src - URL for the largest candidate (assigned to plain src)
267+ # - :src_set - Candidate set with w or x descriptors (if generated)
268+ # - :sizes - sizes attribute value (returned or synthesized as "100vw")
269+ # - :width - Width as a number (if width was provided)
270+ def get_responsive_image_attributes ( options = { } )
271+ # Default breakpoint pools
272+ default_device_breakpoints = [ 640 , 750 , 828 , 1080 , 1200 , 1920 , 2048 , 3840 ]
273+ default_image_breakpoints = [ 16 , 32 , 48 , 64 , 96 , 128 , 256 , 384 ]
274+
275+ # Extract options
276+ src = options [ :src ]
277+ url_endpoint = options [ :url_endpoint ]
278+ width = options [ :width ]
279+ sizes = options [ :sizes ]
280+ device_breakpoints = options [ :device_breakpoints ] || default_device_breakpoints
281+ image_breakpoints = options [ :image_breakpoints ] || default_image_breakpoints
282+ transformation = options [ :transformation ] || [ ]
283+ transformation_position = options [ :transformation_position ]
284+ query_parameters = options [ :query_parameters ]
285+
286+ # Sort and merge breakpoints
287+ sorted_device_breakpoints = device_breakpoints . sort
288+ sorted_image_breakpoints = image_breakpoints . sort
289+ all_breakpoints = ( sorted_image_breakpoints + sorted_device_breakpoints ) . sort . uniq
290+
291+ # Compute candidate widths and descriptor kind
292+ result = compute_candidate_widths (
293+ all_breakpoints : all_breakpoints ,
294+ device_breakpoints : sorted_device_breakpoints ,
295+ explicit_width : width ,
296+ sizes_attr : sizes
297+ )
298+ candidates = result [ :candidates ]
299+ descriptor_kind = result [ :descriptor_kind ]
300+
301+ # Helper to build a single ImageKit URL
302+ build_url_fn = lambda do |w |
303+ build_url (
304+ Imagekit ::Models ::SrcOptions . new (
305+ src : src ,
306+ url_endpoint : url_endpoint ,
307+ query_parameters : query_parameters ,
308+ transformation_position : transformation_position ,
309+ transformation : transformation + [
310+ Imagekit ::Models ::Transformation . new ( width : w , crop : "at_max" ) # never upscale beyond original
311+ ]
312+ )
313+ )
314+ end
315+
316+ # Build srcset
317+ src_set_entries = candidates . map . with_index do |w , i |
318+ descriptor = descriptor_kind == :w ? "#{ w } w" : "#{ i + 1 } x"
319+ "#{ build_url_fn . call ( w ) } #{ descriptor } "
320+ end
321+ src_set = src_set_entries . empty? ? nil : src_set_entries . join ( ", " )
322+
323+ final_sizes = sizes || ( descriptor_kind == :w ? "100vw" : nil )
324+
325+ # Build result - include only when defined
326+ result = {
327+ src : build_url_fn . call ( candidates . last ) # largest candidate
328+ }
329+ result [ :src_set ] = src_set if src_set
330+ result [ :sizes ] = final_sizes if final_sizes
331+ result [ :width ] = width if width
332+
333+ result
334+ end
335+
240336 # @api private
241337 #
242338 # @param client [Imagekit::Client]
@@ -246,6 +342,50 @@ def initialize(client:)
246342
247343 private
248344
345+ # Compute candidate widths for responsive images.
346+ # Implements three strategies:
347+ # 1. Width-based srcSet (w) when sizes attribute contains vw units
348+ # 2. Fallback to device breakpoints when no width or sizes provided
349+ # 3. DPR-based srcSet (x) with 1x and 2x variants when width is provided
350+ def compute_candidate_widths (
351+ all_breakpoints :,
352+ device_breakpoints :,
353+ explicit_width : nil ,
354+ sizes_attr : nil
355+ )
356+ # Strategy 1: Width-based srcSet (w) using viewport vw hints
357+ if sizes_attr
358+ vw_tokens = sizes_attr . scan ( /(?:^|\s )(1?\d {1,2})vw/ ) . flatten . map ( &:to_i )
359+
360+ if vw_tokens . any?
361+ # Find the smallest vw percentage
362+ smallest_ratio = vw_tokens . min / 100.0
363+ # Calculate minimum required pixels
364+ min_required_px = device_breakpoints . first * smallest_ratio
365+ # Filter breakpoints >= min_required_px
366+ candidates = all_breakpoints . select { |bp | bp >= min_required_px }
367+ return { candidates : candidates , descriptor_kind : :w }
368+ end
369+
370+ # No usable vw found: fallback to all breakpoints
371+ return { candidates : all_breakpoints , descriptor_kind : :w }
372+ end
373+
374+ # Strategy 2: Fallback using device breakpoints if no explicit width
375+ return { candidates : device_breakpoints , descriptor_kind : :w } unless explicit_width
376+
377+ # Strategy 3: Use 1x and 2x nearest breakpoints for x descriptor
378+ # Find the first breakpoint >= target (or use the largest)
379+ nearest = lambda do |target |
380+ all_breakpoints . find { |bp | bp >= target } || all_breakpoints . last
381+ end
382+
383+ # Generate unique 1x and 2x variants
384+ unique = [ nearest . call ( explicit_width ) , nearest . call ( explicit_width * 2 ) ] . uniq
385+
386+ { candidates : unique , descriptor_kind : :x }
387+ end
388+
249389 # Generate a 32-character hex token
250390 def generate_token
251391 # Generate 16 random bytes and convert to hex (32 characters)
0 commit comments