Client/Server helpers for dynamic image fetching
By Ryan Romanchuk
On
On
AWS Serverless Image Handler allows any caller to request a known image, applying transformations and edits, on the fly. The only requirement is knowing the source's bucket and key path. Offload image manipulation tasks from a traditional backend, unblocking other teams as requirements change.
An important caveat in this approach is the potential risk of miss-use, resulting in significant decline in cache hits as your cache key is dependent on construction of the base64 encoded edits. Enforce convention by obfuscating url construction and offer traditional pre-planned presets.
Backend
Usage examples:
# app/models/user.rb
class User < ApplicationRecord
include Imageable
end
<%# app/views/users/show.html.erb %>
<h1>My Profile Images</h1>
<p> 200x200 Image</p>
<img src="<%= user.dynamic_square_image %>">
<p>25x24</p>
<img src="<%= user.dynamic_tiny_square_image %>">
<p>Default placeholder/no image</p>
<img src="<%= user.dynamic_placeholder_image %>">
<p>Original</p>
<img src="<%= user.default_dynamic_image %>">
# app/models/concerns/imageable.rb
module Imageable
extend ActiveSupport::Concern
IMAGE_ENDPOINT = Rails.configuration.general.images_endpoint
def dynamic_image(with_edits: {})
return nil if image_key.nil?
IMAGE_ENDPOINT + encoded_path(edits: with_edits)
end
def default_dynamic_image
@default_dynamic_image ||= dynamic_image
end
def dynamic_placeholder_image
@dynamic_placeholder_image ||= IMAGE_ENDPOINT + encoded_path(edits: { key: placeholder_image_key,
bucket: placeholder_image_source_bucket })
end
def placeholder_image_key
'static/user-empty-profile.jpg'
end
def placeholder_image_source_bucket
'my-static-assets'
end
def dynamic_square_image
@dynamic_square_image ||= dynamic_image(with_edits: dynamic_preset_small_square)
end
def dynamic_tiny_square_image
@dynamic_tiny_square_image ||= dynamic_image(with_edits: dynamic_preset_small_tiny)
end
private
# These presets should match the client presets if possible
# @see DynamicImage.swift
def encoded_path(edits: {})
Base64.urlsafe_encode64 request_body.merge(edits).sort.to_h.to_json
end
def request_body
{
bucket: image_source_bucket,
key: image_key
}
end
def dynamic_preset_small_square
{
edits: {
resize: {
width: 200,
height: 200,
fit: 'cover'
}
}
}
end
def dynamic_preset_small_tiny
{
edits: {
resize: {
width: 25,
height: 25,
fit: 'cover'
}
}
}
end
end
iOS Client
Usage examples
class Video: Object {
@Persisted var image_key: String?
@Persisted var image_source_bucket: String?
var indicatorThumbnail: String? {
guard let image_key = image_key, let image_source_bucket = image_source_bucket else { return nil }
return DynamicImage.preset_200x200(imageKey: image_key, imageBucket: image_source_bucket).url
}
}
let thumbnail = Video().indicatorThumbnail
// or
let thumbnail = DynamicImage.preset_200x200(imageKey: 'cool.jpg', imageBucket: 'profile-phots').url
// or
let originalImage = DynamicImage.original(imageKey: 'cool.jpg', imageBucket: 'profile-phots').url
enum DynamicImage {
case original(imageKey: String, imageBucket: String)
case square(imageKey: String, imageBucket: String)
case preset_200x200(imageKey: String, imageBucket: String)
var url: String? {
switch self {
case let .original(imageKey, imageBucket):
let request: Parameters = ["bucket": imageBucket, "key": imageKey]
return buildUrl(request: request)
case let .square(imageKey, imageBucket):
let request: Parameters = ["bucket": imageBucket, "key": imageKey, "edits": edits]
return buildUrl(request: request)
case let .preset_200x200(imageKey, imageBucket):
let request: Parameters = ["bucket": imageBucket, "key": imageKey, "edits": edits]
return buildUrl(request: request)
}
}
private var edits: Parameters {
switch self {
case .preset_200x200:
return ["resize": resize]
case .square:
return ["resize": resize]
default:
return Parameters()
}
}
private var resize: Parameters {
switch self {
case .preset_200x200:
return ["width": 200, "height": 200, "fit": "cover"]
case .square:
return ["width": 200, "height": 200, "fit": "cover"]
default:
return Parameters()
}
}
private func buildUrl(request: Parameters) -> String? {
guard let encodedPath = try? JSONSerialization
.data(withJSONObject: request, options: .sortedKeys)
.base64EncodedString() else {
return nil
}
return "\(C.Web.config.home)/image/\(encodedPath)"
}
}